Ownware
Home›Secreta›Test run
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

Affiliate program
Recommend tools people own — earn 35% on every sale. 90-day tracking, instant delivery, payouts by Lemon Squeezy.
Become an affiliate →