Secreta · Test run · run 2026-09-25
Secreta Test run, as shipped in the download
313 checks passed, 0 failed, when the suite that ships in the zip was run on 2026-09-25 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 honoured
== 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 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) ==
Warning: preg_replace(): Unknown modifier '[' in tests/run.php on line 411
Warning: preg_replace(): Unknown modifier '[' in tests/run.php on line 411
Warning: preg_replace(): Unknown modifier '[' in tests/run.php on line 411
Warning: preg_replace(): Unknown modifier '[' in tests/run.php on line 411
Warning: preg_replace(): Unknown modifier '[' in tests/run.php on line 411
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
Warning: preg_replace(): Unknown modifier '[' in tests/run.php on line 411
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
Warning: preg_replace(): Unknown modifier '[' in tests/run.php on line 411
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, Ical and Gdpr are NOT shipped (P4/P6/P8 are documented N/A)
PASS the API-keys view is handed every column it prints
PASS sw.js cache carries the CURRENT version (static-v3.1.5)
PASS healthz + api_ping + openapi report 3.1.5
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) ==
Warning: preg_replace(): Unknown modifier '[' in tests/run.php on line 411
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 honours
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 licence: every font ships the SIL OFL text for its own family beside it
PASS licence: 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 (27 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: 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 (8)
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)
————————————————————————————————————————————————————
ALL 313 TESTS PASSED (0 failed)
313 assertions
← Back to Secreta · Manual · Quickstart · API