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