Home›Secreta›Test run
Secreta · Test run · run 2026-11-07

Secreta Test run, as shipped in the download

470 checks passed, 0 failed, when the suite that ships in the zip was run on 2026-11-07 with php tests/run.php. Run it yourself after you download.

== 1. generateToken: entropy, URL-safety, uniqueness ==
  PASS  2000 tokens are URL-safe (A-Z a-z 0-9 - _)
  PASS  tokens carry no base64 padding
  PASS  token length is 24 chars (~144 bits entropy)
  PASS  2000 tokens are all unique (got 2000, want 2000)
  PASS  two fresh tokens differ
  PASS  custom byte length widens the token

== 2. isValidToken ==
  PASS  accepts a real token
  PASS  rejects empty
  PASS  rejects slashes (path traversal)
  PASS  rejects dots
  PASS  rejects an over-long token

== 3. isExpired + expiryFromHours ==
  PASS  null expiry never expires
  PASS  empty expiry never expires
  PASS  a past timestamp is expired
  PASS  a far-future timestamp is not expired
  PASS  expiry equal to now counts as expired (<=)
  PASS  one second before now is expired
  PASS  one second after now is not expired
  PASS  expiryFromHours(24) advances exactly 24h (got '2026-01-02 12:00:00', want '2026-01-02 12:00:00')
  PASS  expiryFromHours(1) advances one hour (got '2026-01-01 13:00:00', want '2026-01-01 13:00:00')
  PASS  expiryFromHours(0) means never (null) (got NULL, want NULL)
  PASS  a freshly-made 6h expiry is not yet expired at creation

== 4. nextViewState (burn logic) ==
  PASS  single-view secret burns on first view
  PASS  first of three views does not burn
  PASS  second of three views does not burn
  PASS  third of three views burns
  PASS  overshoot still consumed
  PASS  max_views 0 is guarded (never auto-consumes)

== 5. AES-256-GCM round-trip (mirrors the browser contract) ==
  PASS  encrypt→decrypt recovers plaintext (got 'correct horse battery staple', want 'correct horse battery staple')
  PASS  ciphertext length = plaintext + 16-byte tag (got 44, want 44)
  PASS  ciphertext is not the plaintext
  PASS  wrong key fails (returns null, no exception)
  PASS  wrong iv fails
  PASS  tampered ciphertext fails the auth tag
  PASS  truncated blob (< tag) fails safely
  PASS  empty plaintext round-trips (got '', want '')
  PASS  unicode plaintext round-trips byte-exact (got 'café — ключ — 秘密 — 🔑', want 'café — ключ — 秘密 — 🔑')

== 6. serverKey + at-rest layer ==
  PASS  serverKey is 32 bytes (got 32, want 32)
  PASS  serverKey is deterministic
  PASS  different install secret → different key
  PASS  at-rest output has blob + iv
  PASS  at-rest blob differs from the client ciphertext
  PASS  at-rest decrypt returns the original client ciphertext (got 'YnJvd3Nlci1jaXBoZXJ0ZXh0LWJ5dGVzLWhlcmU=', want 'YnJvd3Nlci1jaXBoZXJ0ZXh0LWJ5dGVzLWhlcmU=')
  PASS  at-rest decrypt with wrong server key fails
  PASS  at-rest decrypt with garbage base64 fails safely

== 7. hashIp (one-way) ==
  PASS  ip hash is 64 hex chars (SHA-256) (got 64, want 64)
  PASS  ip hash is hex only
  PASS  same ip + salt is deterministic
  PASS  different ip → different hash
  PASS  different salt → different hash
  PASS  hash does not contain the raw ip (not reversible by inspection)

== 8. buildStoredRow: NEVER contains plaintext or the key ==
  PASS  stored row does NOT contain the plaintext secret
  PASS  stored row does NOT contain the raw data key
  PASS  stored row has ciphertext_blob / iv / at_rest_iv
  PASS  stored row has NO plaintext-ish field
  PASS  view_count starts at 0 (got 0, want 0)
  PASS  consumed starts at 0 (got 0, want 0)
  PASS  max_views is clamped to at least 1 (got 1, want 1)
  PASS  non-passphrase row has null wrap fields
  PASS  passphrase row keeps wrap_salt + wrapped_key
  PASS  at-rest layer makes the stored blob decrypt back to the browser ciphertext

== 9. buildLogEntry: metadata only, never content ==
  PASS  log entry keys are exactly secret_id/action/ip_hash/at
  PASS  log entry has NO content/plaintext field
  PASS  log entry preserves the hashed ip (got '38ded82c516c46a67f08e14cd2b146f4555a6169f07f92b70be8a283bc862c43', want '38ded82c516c46a67f08e14cd2b146f4555a6169f07f92b70be8a283bc862c43')
  PASS  log entry preserves secret id (got 42, want 42)
  PASS  action 'created' is accepted
  PASS  action 'viewed' is accepted
  PASS  action 'burned' is accepted
  PASS  action 'expired' is accepted
  PASS  action 'not_found' is accepted
  PASS  an unknown action is rejected (throws)
  PASS  a null secret_id is allowed (not_found probes)

== 10. shred: overwrite material for deletion ==
  PASS  shred(0) is empty (got '', want '')
  PASS  shred(100) returns non-empty noise
  PASS  two shreds differ (random)

== Backups card states what the export blanks and keeps ==
  PASS  the Backups card no longer claims an SMTP password (Secreta has no SMTP)
  PASS  ...it names what is blanked and that live share links stay in the file
  PASS  ...which matches the redaction list the export uses

== Show a link as a QR code ==
  PASS  the result card offers "Show as QR code" and loads the bundled qr.js
  PASS  the QR is drawn from the link in the browser, and only when the library loaded
  PASS  the QR path sends nothing anywhere (no fetch / XHR / beacon in it)
  PASS  the recipient is told the QR carries the key

== Backups and restore are for an administrator only ==
  PASS  the backup guard check ran cleanly
  PASS  a signed-in viewer is refused backup_json (403)
  PASS  a signed-in member is refused backup_json (403)
  PASS  a signed-in viewer is refused backup_sqlite (403)
  PASS  a signed-in member is refused backup_sqlite (403)
  PASS  a signed-in viewer is refused restore_page (403)
  PASS  a signed-in member is refused restore_page (403)
  PASS  a signed-in viewer is refused restore_preview (403)
  PASS  a signed-in member is refused restore_preview (403)
  PASS  a signed-in viewer is refused restore_commit (403)
  PASS  a signed-in member is refused restore_commit (403)
  PASS  a signed-in viewer is refused backup_token_mint (403)
  PASS  a signed-in member is refused backup_token_mint (403)
  PASS  an administrator still gets the backup download
  PASS  the scheduled backup answers to its token only, even for a signed-in administrator
  PASS  the Security page shows the Backups card only to someone allowed to download it

== PWA ==
  PASS  manifest.json is valid JSON
  PASS  manifest has required fields
  PASS  manifest URLs are RELATIVE (subpath-install safe)
  PASS  manifest icons exist on disk (192 + 512 + apple-touch)
  PASS  sw.js never caches non-GET
  PASS  sw.js never caches page navigations (auth HTML can not enter cache storage)
  PASS  sw.js cache name carries the version (old caches purged on activate)
  PASS  offline fallback page ships
  PASS  every app layout links the manifest + registers the SW

== Own It 2.0 ==
  PASS  v2: tables
  PASS  v2: totp_cols
  PASS  v2: no_smtp_columns
  PASS  v2: mint
  PASS  v2: revoke
  PASS  v2: totp
  PASS  v2: recovery
  PASS  v2: delivery
  PASS  v2: rate
  PASS  v2: backup_tables
  PASS  v2: serializer_no_content
  PASS  v2: webhook_metadata_only
  PASS  v2: share_url_has_no_key
  PASS  v2: burn_rule
  PASS  v2: routes registered
  PASS  v2: NO list/read secret endpoint exists (listing would defeat the product)
  PASS  v2: the create path refuses plaintext (base64 gate on ciphertext + iv)
  PASS  the create path refuses anything that CANNOT be AES-GCM output
  PASS  v2: the create path reuses the product core (buildStoredRow / expiryFromHours / generateToken)
  PASS  3.0: there is exactly ONE create implementation, and every surface calls it
  PASS  v2: burn path fires metadata-only webhooks
  PASS  v2: 2FA intercepts login + rate limit
  PASS  v2: zero clip() dependency (secreta has no such helper)
  PASS  v2: no ternary key re-reads (PLAN item 10)
  PASS  v2: dark palette + zero hardcoded whites
  PASS  v2: theme.js on ALL full-page layouts + toggle
  PASS  v2: API.md ships (the sw cache version is asserted in the 3.0 block)
  PASS  v2: cli-server base guard present

== Own It 3.0: the sharing policy (pure, offline) ==
  PASS  policy off: TTL untouched (got 720, want 720)
  PASS  policy off: views untouched (got 50, want 50)
  PASS  policy off: nothing reported clamped (got array (
), want array (
))
  PASS  a 30-day request under a 1-day ceiling becomes 1 day (got 24, want 24)
  PASS  **"NEVER" IS THE LONGEST TTL, so a ceiling clamps it too** (got 24, want 24)
  PASS  and that clamp is reported, not silent
  PASS  a compliant TTL passes through untouched (got 6, want 6)
  PASS  a ceiling between presets picks the largest preset within it (got 72, want 72)
  PASS  largestPresetWithin never returns 0 ("never") (got 1, want 1)
  PASS  views are clamped to the ceiling (got 3, want 3)
  PASS  views are always at least 1 (got 1, want 1)
  PASS  views are always at most 100 (got 100, want 100)
  PASS  a multi-view link without a passphrase is REFUSED, not downgraded
  PASS    (downgrading it silently would strand four recipients with a dead link)
  PASS  with a passphrase the same request is allowed
  PASS  a single-view link never needs a passphrase
  PASS  an unknown TTL falls back to the 24h default rather than being honored

== Own It 3.0: MCP · roles · policy · zero-knowledge through restore ==
  PASS  migrate() creates audit_log + invites + saved_views
  PASS  users gain role; settings gain branding, SSO and the three policy columns
  PASS  install() ALONE produces the current schema (no migrate() recursion)
  PASS  migrate() creates the settings singleton row
  PASS  so "UPDATE settings ... WHERE id = 1" is no longer a silent no-op
  PASS  THE MISSING MIGRATE: a 1.x install now actually gains users.totp_secret
  PASS  UPGRADE PATH: drop every v3 table, migrate, they come back
  PASS  3.3.0 one link per person: the link keeps who it is for (spaces tidied)
  PASS  3.3.0 the name is clipped to 120 characters, and only a signed-in sender or a key holder can set one
  PASS  3.3.0 names are cleared 60 days after the link was made (a 59-day-old one is kept)
  PASS  3.3.0 the person opening the link never receives the name (the public fetch does not read it)
  PASS  a member creates secrets, reads the log and may burn
  PASS  but a member cannot touch the sharing policy or the team
  PASS  a viewer reads the log and cannot create or burn
  PASS  a viewer canNOT read the admin trail or the team roster
  PASS  an admin holds the policy
  PASS  AN UPGRADE NEVER LOOSENS: the existing user stays an admin
  PASS  the last admin cannot be demoted into a lockout
  PASS  with no policy set, nothing is clamped
  PASS  a TTL above the ceiling is clamped
  PASS  **"never expires" is clamped by the ceiling, not exempt from it**
  PASS  a compliant TTL is untouched
  PASS  the clamp lands on a real preset
  PASS  views are clamped to the ceiling
  PASS  a multi-view link without a passphrase is refused
  PASS  and allowed with one
  PASS  a one-view link is never blocked by that rule
  PASS  the shared create path mints a secret
  PASS  the returned share_url has NO key fragment — the server cannot build one
  PASS  a plaintext paste is refused, not silently stored unencrypted
  PASS  and the policy is applied by the create path itself, for every surface
  PASS  the stored row contains no plaintext anywhere
  PASS  the at-rest layer really is a second layer of encryption
  PASS  and it round-trips
  PASS  **THE SERVER CANNOT DECRYPT WITH ITS OWN KEY**
  PASS  only the browser key opens the secret
  PASS  the backup no longer carries bcrypt password hashes
  PASS  nor API-key hashes
  PASS  nor two-factor secrets
  PASS  nor webhook signing secrets
  PASS  and it never contained a secret's plaintext
  PASS  the ciphertext DOES survive — a backup that stripped it would restore nothing
  PASS  the secret is genuinely deleted before the restore
  PASS  restore brings it back
  PASS  **AND IT STILL DECRYPTS WITH THE ORIGINAL BROWSER KEY** (the row's test)
  PASS  a redacted credential keeps the value the install already had
  PASS  burning overwrites then clears the ciphertext
  PASS  while keeping the metadata row — the evidence it existed
  PASS  MCP exposes exactly the row's two tools
  PASS  **THERE IS DELIBERATELY NO read_secret TOOL** — reading burns
  PASS  and asking for one is refused as an unknown tool
  PASS  every tool description declares READ-ONLY or WRITES
  PASS  GET /mcp is refused with 405, not 404
  PASS  a nested argument that is not allowlisted is refused
  PASS  create_secret returns a share URL
  PASS  without a key fragment — the agent appends its own
  PASS  and it refuses plaintext, exactly as the form and the API do
  PASS  stats returns counts and no token
  PASS  MCP role gate matches the UI: a viewer key cannot create
  PASS  **NO AUDIT ROW CONTAINS A LIVE SHARE TOKEN**
  PASS  nor any ciphertext or plaintext
  PASS  the metadata filter strips secret keys at ANY depth
  PASS  while still recording THAT a secret was created
  PASS  the webhook payload carries the row id, never the token
  PASS  inkOn() clears 4.5:1 on a very light and a very dark accent
  PASS  a PHP script named .png is refused
  PASS  another product's backup is refused by name stamp
  PASS  OIDC refuses alg=none

== Own It 3.0: wiring (static) ==
  PASS  Database::migrate() now runs at boot (it did not before 3.0)
  PASS  POST and GET /mcp are both routed
  PASS  src includes are idempotent
  PASS  install() reaches the v3 schema without recursing through migrate()
  PASS  EVERY v3 audit write goes through the metadata filter
  PASS  no product code hands AuditLog a raw token, ciphertext or key
  PASS  the webhook payload builder does not emit a token
  PASS  the audit page never prints a full token (it is truncated)
  PASS  there is still NO endpoint that lists or reads secrets
  PASS  bulk burn reuses shred_secret() and there is no bulk DELETE of secrets
  PASS  destructive + policy actions are role-gated
  PASS  the backup redaction list covers every credential
  PASS  BOTH backup paths use that one list
  PASS  the backup does NOT strip the ciphertext (it is what a restore brings back)
  PASS  filled controls take their ink from the branding contrast helper
  PASS  the uploaded logo is actually RENDERED in the layout
  PASS  the branding accent overrides BOTH themes
  PASS  the theme toggle is not nested inside the sign-out button
  PASS  P5 is reachable: the log really exposes filters, saved views and bulk burn
  PASS  the sharing policy has a real UI on the settings page
  PASS  the share form offers only what the policy permits
  PASS  branding reaches the PUBLIC share and view pages, not just the admin app
  PASS  src/Mcp.php is byte-identical to the shared core copy of Mcp.php
  PASS  src/AuditLog.php is byte-identical to the shared core copy of AuditLog.php
  PASS  src/BackupRestore.php is byte-identical to the shared core copy of BackupRestore.php
  PASS  src/Branding.php is byte-identical to the shared core copy of Branding.php
  PASS  src/RolesKit.php is byte-identical to the shared core copy of RolesKit.php
  PASS  src/Oidc.php is byte-identical to the shared core copy of Oidc.php
  PASS  Pdf and Ical are NOT shipped (P4/P6 are documented N/A)
  PASS  4.1.1: Gdpr ships, is byte-identical to the shared core copy and is loaded (data requests)
  PASS  the API-keys view is handed every column it prints
  PASS  sw.js cache carries the CURRENT version (static-v4.1.3)
  PASS  healthz + api_ping + openapi report 4.1.3
  PASS  API.md documents the MCP endpoint AND why read_secret is absent
  PASS  README documents "New in 3.0"
  PASS  README states WHY there is no PDF rather than just omitting one
  PASS  the deploy kit is filled in
  PASS  nav: every sidebar link is highlighted by the page it opens
  PASS  nav: no page highlights a sidebar item that does not exist
  PASS  ip: a direct visitor's forged headers are ignored
  PASS  ip: behind a trusted proxy the forwarded visitor is used
  PASS  ip: CF-Connecting-IP wins over the X-Forwarded-For chain
  PASS  ip: the chain is read right-to-left, past trusted hops
  PASS  ip: a Cloudflare edge is not a trusted proxy by default
  PASS  ip: CIDR maths (v4 boundaries + v6, no cross-family match)

== UI kit: burn state without spending a view (GET /api/secret/{token}/state) ==
  PASS  state: the route exists and is registered BEFORE the consuming fetch
  PASS  state: the probe never writes — no UPDATE, no view_count, no consumed flag
  PASS  state: the probe never hands back ciphertext, IV or key material
  PASS  state: a consumed or unknown token is answered "gone", never described
  PASS  state: an expired secret is shredded on the probe, exactly as the fetch path does
  PASS  state: views_left is derived from the engine's own counters (max_views - view_count)
  PASS  state/engine: last view burns (3 of 3 → 0 left, consumed) (got array (
  'view_count' => 3,
  'consumed' => true,
), want array (
  'view_count' => 3,
  'consumed' => true,
))
  PASS  state/engine: a middle view leaves the secret alive (1 of 3 → 2 left) (got array (
  'view_count' => 2,
  'consumed' => false,
), want array (
  'view_count' => 2,
  'consumed' => false,
))
  PASS  state/engine: a probe on a 1-view secret still reports 1 left — probing is not viewing

== v3.1: the team is managed in the app, not in the database ==
  PASS  team: deactivate/reactivate is routed and needs team.manage + CSRF
  PASS  team: deactivating sets the flag the login query already honors
  PASS  team: you cannot deactivate YOURSELF
  PASS  team: you cannot deactivate the last active admin
  PASS  team/engine: the last-admin guard is the shared RolesKit rule, not a second copy (got array (
  0 => true,
  1 => false,
), want array (
  0 => true,
  1 => false,
))
  PASS  team: re-sending the state it already has changes nothing
  PASS  password: changing your own is routed, CSRF-checked and needs a session
  PASS  password: the current one must be proven before the new one is accepted
  PASS  password: a short one, or the same one again, is refused
  PASS  password: an admin cannot change ANYONE else's password (the route takes no id)
  PASS  password: the audit row records that it changed, never the password itself
  PASS  team: secreta still ships no mailer — an invitation is handed over, never emailed
  PASS  scope: api_keys gains a scope column defaulting to full — an upgrade never loosens
  PASS  scope: requireKey reads the key scope and refuses a write on a read-only key
  PASS  scope: mintKey persists the chosen scope, and anything but read is full access
  PASS  scope: /mcp hands the key scope to the core, which refuses write tools per tool
  PASS  scope: the key form lets an administrator mint a read-only key
  PASS  license: every font ships the SIL OFL text for its own family beside it
  PASS  license: LICENSE.txt is present and carries the IF WE STOP (CONTINUITY) clause

-- F2b: the demo card, the webhook buttons and events, the JSON backup --
  PASS  demo card: /s/Ab3Ab3Ab3Ab3Ab3Ab3Ab3Ab3Ab3Ab3Ab3Ab3Ab3Ab3 says what the page is ("this is the page your recipient opens the link on"), not "signed in as the admin"
  PASS  demo card: / says what the page is ("this is your public share page, open to anyone unless you require sign-in"), not "signed in as the admin"
  PASS  demo card: /secrets keeps the admin wording
  PASS  demo card: /settings keeps the admin wording
  PASS  demo card: /login keeps the admin wording
  PASS  demo card: below 600px the script starts it as the small "Own it" pill unless the visitor opened it this session
  PASS  demo card: output without </body> (JSON, CSV, a PDF) passes through untouched
  PASS  forms: every POST form on a screen reaches a registered route (37 checked)
  PASS  webhooks: the Settings page answers and lists every event it offers
  PASS  webhooks: the Add, Pause, Resume and Delete buttons each reach a route and do what they say
  PASS  webhooks: Delete also removes that webhook's delivery log, as its confirmation says
  PASS  webhooks: a paused webhook is sent nothing
  PASS  webhooks: request.answered is sent by the action where it happens [200,200]
  PASS  webhooks: secret.created is sent by the action where it happens [200]
  PASS  webhooks: secret.viewed is sent by the action where it happens [200]
  PASS  webhooks: secret.burned is sent by the action where it happens [200]
  PASS  webhooks: every event the Settings page offers is sent somewhere
  PASS  webhooks: burning a live secret from the Secrets page sends secret.burned
  PASS  webhooks: that announcement says no views are left and carries no token
  PASS  backup redaction: a credential was planted in every credential column (12)
  PASS  backup redaction: /backup.json and the scheduled backup both use the one list, SC_REDACT
  PASS  backup redaction: /backup.json (downloaded by an administrator) carries none of the planted credentials
  PASS  backup redaction: /backup.json (downloaded by an administrator) shows [REDACTED] in each credential column and leaves none unredacted
  PASS  backup redaction: the scheduled backup file carries none of the planted credentials
  PASS  backup redaction: the scheduled backup file shows [REDACTED] in each credential column and leaves none unredacted
  PASS  backup redaction: the administrator got the download (HTTP 200)

== 3.2.0 · request links: the protocol (PHP reference) ==
  PASS  a public key is a 65-byte uncompressed P-256 point (got array (
  0 => 65,
  1 => '',
), want array (
  0 => 65,
  1 => '',
))
  PASS  it validates as a point on the curve
  PASS  the answer carries ciphertext, a 12-byte nonce and the sender's one-off public key (got array (
  0 => 12,
  1 => 65,
), want array (
  0 => 12,
  1 => 65,
))
  PASS  the ciphertext is not the plaintext
  PASS  the asker's private key opens it (got 'admin / Tr0ub4dor&3', want 'admin / Tr0ub4dor&3')
  PASS  another request's token does not (the key is bound to the request) (got NULL, want NULL)
  PASS  another private key does not (got NULL, want NULL)
  PASS  a changed byte is refused (GCM tag) (got NULL, want NULL)
  PASS  a point off the curve is refused
  PASS  a compressed point, a short one and non-base64url are refused
  PASS  an answer shaped like plaintext is refused before storage
  PASS  the browser uses the same protocol words: P-256, HKDF-SHA-256 salted with the token, info "secreta-request-v1", AES-256-GCM
  PASS  the browser never writes a decrypted answer as HTML
  PASS  the asker's private key is checked against the request before the one read is spent

== 3.2.0 · request links over HTTP (php -S, real logins, a sender with no account) ==
  PASS  requests: the app answers under php -S
  PASS  requests: a member signs in through the login form
  PASS  requests: Request a secret opens, with the form and the browser script
  PASS  requests: a request says what it asks for (empty label: 422)
  PASS  requests: a public key that is not a P-256 point is refused (422)
  PASS  requests: creating a request without the right CSRF token is refused (419)
  PASS  requests: a request is created: a public /r/ link and a private /open link
  PASS  requests: the server holds the public key, never the private one
  PASS  requests: it expires when asked (3 days)
  PASS  requests: the person asked opens the link with no account and sees what is asked for and the key to encrypt to
  PASS  requests: an answer shaped like plaintext is refused (422)
  PASS  requests: an encrypted answer is accepted
  PASS  requests: a second answer on the same link is refused (410)
  PASS  requests: the link then says it is closed
  PASS  requests: the answer is stored encrypted again at rest (not the ciphertext as sent)
  PASS  requests: another member cannot open someone else's request page (404)
  PASS  requests: ... nor fetch its answer (404)
  PASS  requests: a viewer cannot use requests (403)
  PASS  requests: the asker's open page carries the request's public key for the check
  PASS  requests: the asker fetches the answer once
  PASS  requests: and the private key from their link opens it, exactly as sent
  PASS  requests: a second read is refused (410)
  PASS  requests: the stored answer is destroyed after the read
  PASS  requests: withdrawing an answered request destroys the answer
  PASS  requests: an expired request refuses an answer and says it has expired
  PASS  requests: the sharing policy's longest life applies to request links (30 days → 1 day)
  PASS  requests: made, answered, opened and withdrawn are in the admin trail, with no secret in it
  PASS  requests: (overflow) "save as a template" keeps the ask and how long, once per label
  PASS  requests: (overflow) a template pre-fills what is asked and for how long
  PASS  requests: (overflow) the requests page lists the templates
  PASS  requests: (overflow) another member cannot remove someone else's template
  PASS  requests: (overflow) the one who saved it can

== 3.2.0 pass B · a file with a secret (encrypted in the browser, off by default) ==
  PASS  file: the throwaway install answers
  PASS  file: off by default: no file field on the share page
  PASS  file: and a file sent anyway is refused
  PASS  file: switched on: the field shows the cap (1 MB here)
  PASS  file: a multipart upload is stored with its secret
  PASS  file: on disk it is encrypted again at rest (nonce + ciphertext + tag), never the bytes uploaded
  PASS  file: the file is readable by the app alone (0600)
  PASS  file: the server holds no file name, on disk or in the row
  PASS  file: the link's page says a file is included, before anything is revealed
  PASS  file: the link's key opens the envelope (the name) and the file, byte for byte
  PASS  file: the last view shreds the file with the text
  PASS  file: the file needs its own nonce, not the text's
  PASS  file: a file shorter than AES-GCM output is refused
  PASS  file: a file over the cap is refused, saying so
  PASS  file: the REST API takes a file as base64 (the command-line tool's way)
  PASS  file: two views both carry the file, then it is gone
  PASS  file: an expired secret nobody opened loses its file at the next create
  PASS  file: data/files is not served

== 3.2.0 pass B · bin/secreta-send, the command-line tool ==
  PASS  cli: it encrypts on the sender's machine, the server stores no plaintext, and the printed link's key opens it (2 views asked, 2 stored)
  PASS  cli: with a passphrase the link carries its own key and the data key is wrapped (PBKDF2-SHA-256, 150,000 rounds, as the browser)
  PASS  cli: it sends a file with a note; the key opens both, the name inside the envelope
  PASS  cli: a wrong API key: exit 1 and the server's answer
  PASS  cli: nothing piped in: exit 1, saying so
  PASS  backup: a passphrase secret's wrapped key in /backup.json does not open with the right passphrase alone: the link's key is part of the wrapping, and it never reaches the server
  PASS  backup: its ciphertext in /backup.json is under the server's at-rest key (the config file, not the database): the data key alone does not open it
  (node is not on this PATH: the browser-code interop check runs where node is, as the release run does)
  (node is not on this PATH: run `node tests/strip_meta.test.js` for the metadata stripper's own checks)
  (node is not on this PATH: run `node tests/wrap_key.test.js` for the browser's key wrapping)
  (node is not on this PATH: run `node tests/people.test.js` for the per-person names)
  PASS  the decrypted file is always saved as a download, never opened as a page on this site

== 3.3.0 round 4 (customer voice) · one link per person ==
  PASS  the share page offers "one link per person" to a signed-in sender only
  PASS  each person's link is encrypted on its own (its own key) and sent with the name
  PASS  the links are made one after another; a refusal keeps the links already made and says how many
  PASS  the team's list and the record show who a link was for

————————————————————————————————————————————————————

== 4.0: the Update button is wired ==
  PASS  updates: the release public key is pinned
  PASS  updates: Database::migrate() creates the updater table (in code, not in a comment)
  SKIP  updates: the state row (this suite keeps no open database in its own process; update_e2e.sh installs, migrates and updates for real)
  PASS  updates: index.php clears stale compiled code before anything else loads
  PASS  updates: index.php finishes or rolls back an update before any controller loads, with this app's canary
  PASS  updates: route get /updates is updates_page()
  PASS  updates: route post /updates/key is updates_key_save()
  PASS  updates: route post /updates/check is updates_check()
  PASS  updates: route post /updates/apply is updates_apply()
  PASS  updates: updates_page() opens with the restore page's admin gate
  PASS  updates: updates_key_save() opens with the restore page's admin gate
  PASS  updates: updates_check() opens with the restore page's admin gate
  PASS  updates: updates_apply() opens with the restore page's admin gate
  PASS  updates: every POST checks the CSRF token
  PASS  updates: the page is told why updates are off here (Updater::whyOff, F5 A1 pass)
  PASS  updates: the menu links to the page
  PASS  updates: bin/update.php refuses the web and finishes in a fresh process
  PASS  updates: where updates are off (Cloud, the demo, a missing extension) the page says why and offers no button
  PASS  updates: with a key and a newer signed release the page offers "Back up and update"

== 4.0 A2: sample data, backups, health ==
  PASS  A2: the owner's-install checks ran to the end
  PASS  A2: an owner's own install (no Cloud config, no demo marker)
  PASS  A2: a fresh install's sample tables are all empty, so the installer's sample data can go in
  PASS  A2: switched off: an install without "Start with sample data" gets none
  PASS  A2: first request: the marker is used once and the sample business is there
  PASS  A2: every one of the owner's settings is theirs, not the sample's
  PASS  A2: the sample logins cannot sign in: no password matches, and switched off where the product can
  PASS  A2: the owner's own login is untouched
  PASS  A2: the load is in the audit log
  PASS  A2: a second marker on a Secreta with records seeds nothing and changes nothing
  PASS  A2: real use: one of the owner's own rows among the sample
  PASS  A2: remove: the owner's own row stays, and a sample row it points at is kept
  PASS  A2: remove: the report says what went, and what was kept because the owner's records use it
  PASS  A2: remove: the rest of the sample is gone
  PASS  A2: remove: every kept sample row keeps all its own parts
  PASS  A2: remove: no row is left pointing at a row that was removed
  PASS  A2: remove: the sample logins are gone but the ones kept rows use, the owner stays, and nothing counts as sample any more
  PASS  A2: remove: the sample's files are gone from disk (0 written; one a row that stays names stays with it)
  PASS  A2: remove: the owner's own uploaded file is untouched
  PASS  A2: "Load demo data" on a Secreta with records refuses and deletes nothing
  PASS  A2: backup: written, sent to the bucket (a signed PUT), and read back
  PASS  A2: backup: the bucket secret is not in the backup it protects
  PASS  A2: backup: under the standard keep count (14) every backup is kept
  PASS  A2: switched off: with automatic backups off, a backup is never due however old the last one
  PASS  A2: the backup lines for Backup & restore: last backup fine, offsite fine, read-back fine
  PASS  A2: an offsite endpoint must be https for an owner (plain http only in tests)
  PASS  A2: all 25 owner's-install checks reported
  PASS  A2: Database::migrate() creates the sample-data and backup tables (in code, not in a comment)
  PASS  A2: index.php loads the three cores and the wiring
  PASS  A2: the first request seeds the installer's sample, and an owner's install backs itself up when due
  PASS  A2: route post /sample-data/remove is sample_data_remove()
  PASS  A2: route post /restore/backups is backups_save()
  PASS  A2: route post /restore/backup-now is backup_now()
  PASS  A2: route get /health is health_page()
  PASS  A2: sample_data_remove() opens with the restore page's admin gate
  PASS  A2: backups_save() opens with the restore page's admin gate
  PASS  A2: backup_now() opens with the restore page's admin gate
  PASS  A2: health_page() opens with the restore page's admin gate
  PASS  A2: every POST checks the CSRF token
  PASS  A2: off an owner's install (Cloud, the demo) the backup settings and "Back up now" refuse in words
  PASS  A2: the installer offers sample data, ticked, and only leaves the first-run marker (with every table's row count)
  PASS  A2: the first run is strictly once (a marker that cannot be removed seeds nothing), and treats the installer's rows as its defaults only while nothing has changed
  PASS  A2: the scheduled-backup URL runs the same backup (offsite copy and read-back included)
  PASS  A2: "Load demo data" outside the public demo goes through SampleData (only into an empty Secreta, removable)
  PASS  A2: the menu links to Health
  PASS  A2: the sample-data banner shows only to an admin, only while sample data is there, with its Remove button, and stays put (nested: the UI kit toasts a bare .main > .alert away)
  PASS  A2: Backup & restore shows the automatic backups, "Back up now" and the bucket settings (none on Cloud)
  SKIP  A2: the Health page render (this suite loads no view helpers in its own process; the owner's-install check and update_e2e.sh run the real app)
  PASS  erase planting · sqlite: the subject is created and a marker is planted in all 2 personal columns
  PASS  erase planting · sqlite: the app's own erase runs through its route and reports no failure
  PASS  erase planting · sqlite: no planted marker survives anywhere (19 tables dumped)
  PASS  erase planting · sqlite: the links stay as they were (token, ciphertext, views): only the name goes
  PASS  erase planting · sqlite: a member cannot erase: HTTP 403, nothing changed
  PASS  erase planting · sqlite: a member cannot open the Data requests page: HTTP 403
  PASS  erase planting · sqlite: a viewer cannot erase: HTTP 403, nothing changed
  PASS  erase planting · sqlite: a viewer cannot open the Data requests page: HTTP 403
  PASS  erase planting · sqlite: an admin opens the Data requests page and the name is listed
  PASS  erase planting · sqlite: the export lists the link with its name and never its token or ciphertext
ALL 470 TESTS PASSED (0 failed)
470 assertions

← Back to Secreta · Manual · Quickstart · API