Ownware
Home›Supplia›Test run
Supplia · Test run · run 2026-09-25

Supplia Test run, as shipped in the download

360 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. money: parse_cents / fmt_cents / clamp (contract values are integer cents) ==
  PASS  "12,000.00" -> 1200000 (got 1200000, want 1200000)
  PASS  "1,250" -> 125000 (got 125000, want 125000)
  PASS  "$8" -> 800 (got 800, want 800)
  PASS  "" -> null (got NULL, want NULL)
  PASS  "1.239" -> null (3dp) (got NULL, want NULL)
  PASS  1200000 -> "12,000.00" (got '12,000.00', want '12,000.00')
  PASS  0 -> "0.00" (got '0.00', want '0.00')
  PASS  15-digit clamps to 2e9 (got 2000000000, want 2000000000)
  PASS  clamp_cents floors at 0 (got 0, want 0)
  PASS  clamp_cents caps at 2e9 (got 2000000000, want 2000000000)

== 2. Supply::money + normalizeCurrency + enum normalizers/labels ==
  PASS  $12,000.00 (got '$12,000.00', want '$12,000.00')
  PASS  -$45.00 (sign before symbol) (got '-$45.00', want '-$45.00')
  PASS  EUR symbol (got '€250.00', want '€250.00')
  PASS  ZAR code prefix (got 'ZAR 200.00', want 'ZAR 200.00')
  PASS  normalize "usd" -> USD (got 'USD', want 'USD')
  PASS  normalize junk -> USD (got 'USD', want 'USD')
  PASS  normalizeCategory upper (got 'it_software', want 'it_software')
  PASS  normalizeCategory junk -> other (got 'other', want 'other')
  PASS  categoryLabel logistics (got 'Logistics & Shipping', want 'Logistics & Shipping')
  PASS  categoryLabel junk -> Other (got 'Other', want 'Other')
  PASS  normalizeVendorStatus junk -> active (got 'active', want 'active')
  PASS  vendorStatusLabel inactive (got 'Inactive', want 'Inactive')
  PASS  normalizeBaseStatus CLOSED -> closed (got 'closed', want 'closed')
  PASS  normalizeBaseStatus expiring -> active (never stored) (got 'active', want 'active')
  PASS  normalizeBaseStatus junk -> active (got 'active', want 'active')
  PASS  contractStatusLabel expiring (got 'Expiring', want 'Expiring')
  PASS  contractStatusLabel expired (got 'Expired', want 'Expired')
  PASS  contractStatusLabel junk -> Active (got 'Active', want 'Active')

== 3. sumCents + rollup: by-category spend, in PHP integer cents (overflow-clamped) ==
  PASS  sumCents of 3 = 504000 (got 504000, want 504000)
  PASS  sumCents empty -> 0 (got 0, want 0)
  PASS  sumCents missing key -> 0 (got 0, want 0)
  PASS  sumCents overflow clamps to 2e9 (got 2000000000, want 2000000000)
  PASS  rollup it_software = 144000 (got 144000, want 144000)
  PASS  rollup facilities = 450000 (got 450000, want 450000)
  PASS  rollup logistics = 240000 (got 240000, want 240000)
  PASS  rollup has exactly 3 groups (got 3, want 3)
  PASS  rollup grand total reconciles (got 834000, want 834000)
  PASS  rollup per-group overflow clamps to 2e9 (got 2000000000, want 2000000000)

== 4. DATE MATH — daysUntil: exact across day/month/year/leap/DST boundaries ==
  PASS  same day -> 0 (got 0, want 0)
  PASS  tomorrow -> +1 (got 1, want 1)
  PASS  yesterday -> -1 (got -1, want -1)
  PASS  month boundary Jul31->Aug01 = 1 (got 1, want 1)
  PASS  year boundary Dec31->Jan01 = 1 (got 1, want 1)
  PASS  non-leap Feb28->Mar01 = 1 (2026) (got 1, want 1)
  PASS  leap Feb28->Mar01 = 2 (2028) (got 2, want 2)
  PASS  DST spring-forward Mar07->Mar09 = 2 (got 2, want 2)
  PASS  DST fall-back Oct31->Nov02 = 2 (got 2, want 2)
  PASS  full year Jan01->Dec31 = 364 (got 364, want 364)
  PASS  reverse full year = -364 (got -364, want -364)
  PASS  two years incl leap = 731 (got 731, want 731)

== 5. daysToDate + expiryState: window classification at every threshold edge (60/30/7) ==
  PASS  daysToDate today = 0 (got 0, want 0)
  PASS  daysToDate +7 = 7 (got 7, want 7)
  PASS  daysToDate -1 = -1 (got -1, want -1)
  PASS  daysToDate empty = null (got NULL, want NULL)
  PASS  daysToDate invalid = null (got NULL, want NULL)
  PASS  no date -> none (got 'none', want 'none')
  PASS  -1 day -> expired (got 'expired', want 'expired')
  PASS  0 days (today) -> critical (got 'critical', want 'critical')
  PASS  exactly 7 -> critical (got 'critical', want 'critical')
  PASS  8 -> warning (got 'warning', want 'warning')
  PASS  exactly 30 -> warning (got 'warning', want 'warning')
  PASS  31 -> notice (got 'notice', want 'notice')
  PASS  exactly 60 -> notice (got 'notice', want 'notice')
  PASS  61 -> ok (got 'ok', want 'ok')
  PASS  state carries the day count (got array (
  'state' => 'critical',
  'days' => 3,
), want array (
  'state' => 'critical',
  'days' => 3,
))
  PASS  mis-ordered thresholds still classify sanely (got 'warning', want 'warning')

== 6. isExpiryAlert + expiryPhrase (end date) + renewalPhrase wording ==
  PASS  expired is an alert
  PASS  critical is an alert
  PASS  notice is an alert
  PASS  ok is NOT an alert
  PASS  none is NOT an alert
  PASS  expiry phrase none (got 'No end date', want 'No end date')
  PASS  expiry phrase expires today (got 'Expires today', want 'Expires today')
  PASS  expiry phrase in 1 day (got 'Expires in 1 day', want 'Expires in 1 day')
  PASS  expiry phrase in 2 days (got 'Expires in 2 days', want 'Expires in 2 days')
  PASS  expiry phrase expired 1 day ago (got 'Expired 1 day ago', want 'Expired 1 day ago')
  PASS  expiry phrase expired 3 days ago (got 'Expired 3 days ago', want 'Expired 3 days ago')
  PASS  renewal phrase none (got 'No renewal date', want 'No renewal date')
  PASS  renewal phrase renews today (got 'Renews today', want 'Renews today')
  PASS  renewal phrase in 1 day (got 'Renews in 1 day', want 'Renews in 1 day')
  PASS  renewal phrase in 5 days (got 'Renews in 5 days', want 'Renews in 5 days')
  PASS  renewal phrase was 1 day ago (got 'Renewal was 1 day ago', want 'Renewal was 1 day ago')
  PASS  renewal phrase was 12 days ago (got 'Renewal was 12 days ago', want 'Renewal was 12 days ago')

== 7. STATUS DERIVATION: active/expiring/expired/closed from base + end date vs today ==
  PASS  closed + future end -> closed (got 'closed', want 'closed')
  PASS  closed + past end -> closed (got 'closed', want 'closed')
  PASS  closed + no end -> closed (got 'closed', want 'closed')
  PASS  active + no end -> active (got 'active', want 'active')
  PASS  active + end yesterday -> expired (got 'expired', want 'expired')
  PASS  active + end today -> expiring (not expired) (got 'expiring', want 'expiring')
  PASS  active + end +7 (critical) -> expiring (got 'expiring', want 'expiring')
  PASS  active + end +30 (warning) -> expiring (got 'expiring', want 'expiring')
  PASS  active + end +60 (notice edge) -> expiring (got 'expiring', want 'expiring')
  PASS  active + end +61 (beyond notice) -> active (got 'active', want 'active')
  PASS  active + end far future -> active (got 'active', want 'active')
  PASS  boundary end today != expired (got true, want true)
  PASS  boundary end yesterday == expired (got 'expired', want 'expired')
  PASS  junk base + expired end -> expired (got 'expired', want 'expired')

== 8. Supply::csvCell: numeric-aware formula-injection guard ==
  PASS  =SUM neutralised (got '\'=SUM(A1)', want '\'=SUM(A1)')
  PASS  +1 neutralised (got '\'+1', want '\'+1')
  PASS  -SUM neutralised (got '\'-SUM(A1)', want '\'-SUM(A1)')
  PASS  @cmd neutralised (got '\'@cmd', want '\'@cmd')
  PASS  leading TAB neutralised (got '\'	x', want '\'	x')
  PASS  cmd payload neutralised (got '\'=cmd|calc', want '\'=cmd|calc')
  PASS  CR payload quoted+prefixed (got '"\'
X"', want '"\'
X"')
  PASS  -45.00 kept numeric (got '-45.00', want '-45.00')
  PASS  -2 kept numeric (got '-2', want '-2')
  PASS  1250 kept numeric (got '1250', want '1250')
  PASS  plain text untouched (got 'Northwind Software Ltd', want 'Northwind Software Ltd')
  PASS  mid-string = untouched (got 'a=b', want 'a=b')
  PASS  comma/quote RFC-4180 quoted (got '"Net 30, ""monthly"""', want '"Net 30, ""monthly"""')

== 9. Supply::toCsv: header + rows, RFC-4180, CRLF ==
  PASS  header row (got 'Vendor,Terms,Value', want 'Vendor,Terms,Value')
  PASS  row count (header+2) (got 3, want 3)
  PASS  CRLF line endings
  PASS  comma field quoted
  PASS  negative value stays numeric in CSV

== 10. csrf_valid: empty-token bypass rejected (hash_equals('','') is TRUE) ==
  PASS  empty stored + empty given -> REJECTED
  PASS  empty stored + any given -> REJECTED
  PASS  matching token -> accepted
  PASS  mismatched token -> rejected

== 11. safe_url: only http/https references are linkifiable ==
  PASS  https url kept (got 'https://drive.example.com/x.pdf', want 'https://drive.example.com/x.pdf')
  PASS  http url kept (got 'http://intranet/contract', want 'http://intranet/contract')
  PASS  javascript scheme rejected (got '', want '')
  PASS  plain filing ref rejected (got '', want '')
  PASS  empty -> empty (got '', want '')

== 12. Pdf: produces a valid, non-trivial PDF document ==
  PASS  starts with %PDF header
  PASS  ends with %%EOF
  PASS  has xref table
  PASS  has a page object
  PASS  non-trivial length
  PASS  long line wraps to 2+ chunks

== 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: smtp_cols
  PASS  v2: mint
  PASS  v2: revoke
  PASS  v2: totp
  PASS  v2: recovery
  PASS  v2: delivery
  PASS  v2: rate
  PASS  v2: csv_dry
  PASS  v2: csv_suppliers
  PASS  v2: csv_contracts_matched
  PASS  v2: derived_status
  PASS  v2: status_not_stored
  PASS  v2: contract_serializer
  PASS  v2: contract_serializer_nocompute
  PASS  v2: normalizers
  PASS  v2: backup_tables
  PASS  v2: migrate() runs on every boot — pre-2.0 this product only built its schema in the installer
  PASS  v2: routes registered (api, 2fa, security, backup, import, healthz)
  PASS  v2: 2FA intercepts login + login is rate-limited
  PASS  v2: notifications + webhooks fire on the BROWSER path too, never fatally
  PASS  v2: API reuses the tested core (no re-derived guards)
  PASS  v2: renewing-within filter exists and never re-derives its own windows
  PASS  v2: dark palette + no hardcoded white backgrounds outside @media print
  PASS  v2: theme.js + qr.js ship and layout loads theme.js + a toggle
  PASS  v2: shared modules are copy-identical to the shared core (never forked per app)
  PASS  v3: API.md ships + sw cache is v3.1.5

== Own It 3.0 ==
  PASS  v3: v3_tables
  PASS  v3: users_role_column
  PASS  v3: upgrade_dropped_ok
  PASS  v3: migrate_restores_v3_tables
  PASS  v3: upgrade_keeps_admin_rights
  PASS  v3: role_viewer_cannot_write
  PASS  v3: role_member_can_write
  PASS  v3: role_admin_wildcard
  PASS  v3: unknown_role_fails_closed
  PASS  v3: last_admin_guard
  PASS  v3: shared_writer_creates
  PASS  v3: writer_audits
  PASS  v3: writer_refuses_nameless
  PASS  v3: mcp_spec_uses_core_keys
  PASS  v3: mcp_five_tools
  PASS  v3: mcp_descriptions_state_writes
  PASS  v3: mcp_category_is_enum
  PASS  v3: mcp_enum_matches_product
  PASS  v3: argguard_refuses_nested
  PASS  v3: argguard_refuses_unknown
  PASS  v3: argguard_accepts_valid
  PASS  v3: audit_append_only
  PASS  v3: audit_redacts_secrets
  PASS  v3: spend_matches_core_sum
  PASS  v3: pdf_engine_renders
  PASS  v3: legacy_pdf_still_renders
  PASS  v3: two_distinct_pdf_classes
  PASS  v3: pdf_uses_product_money_helper
  PASS  v3: pdf_does_no_arithmetic
  PASS  v3: pdf_exports_are_audited
  PASS  v3: ical_token_roundtrip
  PASS  v3: ical_empty_hash_refused
  PASS  v3: feed_covers_contracts_and_documents
  PASS  v3: feed_uids_are_unique_and_stable
  PASS  v3: feed_is_crlf_only
  PASS  v3: feed_every_event_has_uid
  PASS  v3: feed_skips_undated_rows
  PASS  v3: sup_documents_can_belong_to_a_contract
  PASS  v3: sup_both_upload_doors_share_one_store
  PASS  v3: sup_contract_upload_is_gated_and_scoped
  PASS  v3: sup_a_renamed_text_file_is_not_a_contract
  PASS  v3: sup_contract_files_are_scoped_to_their_contract
  PASS  v3: sup_vendor_documents_are_untouched_by_contract_files
  PASS  v3: sup_removing_a_file_leaves_the_contract_alone
  PASS  v3: sup_serving_a_document_re_checks_the_bytes
  PASS  v3: sup_no_screen_still_denies_file_storage
  PASS  v3: sup314_notice_moves_the_radar
  PASS  v3: sup314_notice_is_read_from_the_form_and_clamped
  PASS  v3: sup314_feed_has_the_notice_date_and_skips_closed
  PASS  v3: sup314_api_reports_notice_and_cancel_by
  PASS  v3: sup_feed_covers_renewals_not_only_endings
  PASS  v3: sup_feed_carries_no_contacts_and_no_money
  PASS  v3: sup_feed_token_is_stored_hashed_and_checked_by_hash
  PASS  v3: sup_a_revoked_feed_token_is_out_of_the_live_set
  PASS  v3: sup_feed_route_refuses_a_bad_token
  PASS  v3: sup_the_feed_finally_has_a_door
  PASS  v3: sup_the_feed_url_is_built_once
  PASS  v3: gdpr_export_returns_full_rows
  PASS  v3: gdpr_anonymize_clears_contact
  PASS  v3: gdpr_keeps_company_row
  PASS  v3: gdpr_keeps_contract_history
  PASS  v3: gdpr_reports_rows_touched
  PASS  v3: gdpr_audit_is_count_only
  PASS  v3: restore_refuses_other_app
  PASS  v3: restore_refuses_non_backup
  PASS  v3: restore_parses_own_backup
  PASS  v3: diff_writes_nothing
  PASS  v3: diff_reports_updates
  PASS  v3: commit_applies_preview
  PASS  v3: redacted_never_overwrites
  PASS  v3: scheduled_backup_writes
  PASS  v3: scheduled_backup_redacts
  PASS  v3: prune_keeps_retention
  PASS  v3: commit_requires_preview_token
  PASS  v3: restore_paths_are_audited
  PASS  v3: branding_accepts_real_png
  PASS  v3: branding_returns_plain_filename
  PASS  v3: branding_content_type_from_bytes
  PASS  v3: branding_refuses_disguised_script
  PASS  v3: branding_refuses_svg
  PASS  v3: accent_validates_hex
  PASS  v3: ink_on_accent_is_AA
  PASS  v3: fixture_vendor_has_contracts
  PASS  v3: remove_archives_when_history_exists
  PASS  v3: remove_deletes_when_no_history
  PASS  v3: remove_audits_both_outcomes
  PASS  v3: bulk_reuses_removal_rule
  PASS  v3: bulk_reuses_vendor_writer
  PASS  v3: bulk_is_transactional
  PASS  v3: saved_view_pages_allowlisted
  PASS  v3: sso_unconfigured_is_null
  PASS  v3: sso_configured_when_complete
  PASS  v3: sso_partial_config_refused
  PASS  v3: sso_rejects_garbage_token
  PASS  v3: sso_failures_return_to_local_login
  PASS  v3: sso_does_not_autocreate_accounts
  PASS  v3: sso_uses_same_session_creation
  PASS  v3: scorecard_has_no_ontime_metric
  PASS  v3: scorecard_page_reuses_function
  PASS  v3: invite_token_roundtrip
  PASS  v3: invite_expiry_enforced
  PASS  v3: invite_lookup_finds_live
  PASS  v3: invite_lookup_rejects_bad
  PASS  v3: invite_is_single_use
  PASS  v3: last_admin_cannot_be_demoted
  PASS  v3: role_change_calls_the_guard
  PASS  v3: invite_accept_needs_no_session
  PASS  v3: db_files_cleaned
  PASS  v3: /healthz answers BEFORE the install guard (deploy-kit healthcheck)
  PASS  v3: healthz reports whether the install has run
  PASS  v3: every v3 route has its view template
  PASS  v3: deploy kit filled for this product
  PASS  v3: every version surface reports 3.1.5
  PASS  v3: migrate() runs at boot after the schemaExists guard
  PASS  v3: /mcp route registered and API-key guarded first
  PASS  v3: audit viewer is read-only (no edit/delete route exists)
  PASS  v3: audit view reads the core's `at` column, never created_at
  PASS  v3: requireKey refuses a deactivated user's key (u.active = 1 in the lookup)
  PASS  v3: legacy PDF writer moved aside, core engine present, both load
  PASS  v3: all 9 shared cores are present in src/
  PASS  v3: cores byte-identical to the shared core (catalog root reachable)
  PASS  v3: the layout INJECTS the brand CSS (a helper nothing calls brands nothing)
  PASS  v3: the branding form exists and posts to the branding route
  PASS  v3: the logo is rendered, not merely stored
  PASS  v3: the brand accent is written for BOTH themes (dark re-declares --accent and outranks :root)
  PASS  v3: ONE redaction list covers the download AND the scheduled backup
  PASS  v3: no backup writer carries a redaction list of its own
  PASS  v3: migrate() ensures the settings singleton (an UPDATE … WHERE id = 1 needs a row)
  PASS  v3: GET /mcp answers the transport hint BEFORE the key check (settings relay item 3)
  PASS  v3: the deploy kit ships INSIDE app/ (package.sh only packages app/ + the root docs)
  PASS  v3: every api-settings key/webhook action is admin-gated (_sup_require('*')), not merely logged in

== 3.1: renewal radar ==
  PASS  extend: +12 months moves the end date a year out (got '2027-08-01', want '2027-08-01')
  PASS  extend: the renewal date moves by the same span (the notice window keeps its shape) (got '2027-07-20', want '2027-07-20')
  PASS  extend: month-end clamps (31 Jan +1 month is the last day of February, never 3 March) (got '2026-02-28', want '2026-02-28')
  PASS  extend: a leap February clamps to the 29th (got '2028-02-29', want '2028-02-29')
  PASS  extend: a LAPSED contract restarts from today, never back-dated into "extended and expired"
  PASS  extend: a nonsense span is refused with a sentence, not clamped silently
  PASS  extend: a contract with no end date is refused (there is nothing to push out)
  PASS  the extend endpoint delegates to Supply::extendDates and computes no dates of its own
  PASS  the extend endpoint gates, CSRF-checks and audits before/around the write
  PASS  the extend endpoint answers a JS-less form with a redirect, not a raw JSON body
  PASS  the radar buckets by the product's own configured windows, inventing no numbers
  PASS  v3: no ungated write route beyond the by-design baseline (router-derived)
  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)
  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
  PASS  notice: 60 days before a 31 Dec renewal is 1 Nov (got '2026-11-01', want '2026-11-01')
  PASS  notice: with no renewal date it counts back from the end date (got '2026-11-01', want '2026-11-01')
  PASS  notice: across a leap day, 1 day before 1 Mar 2028 is 29 Feb (got '2028-02-29', want '2028-02-29')
  PASS  notice: no notice period, or no date to count back from, gives no deadline
  PASS  notice: phrase on the day (got 'Last day to give notice is today', want 'Last day to give notice is today')
  PASS  notice: phrase after the day (got 'Notice deadline passed 3 days ago', want 'Notice deadline passed 3 days ago')
  PASS  notice: the contract form asks for the notice period, and the column migrates in
  PASS  backup: the JSON and the raw .sqlite backup downloads take the admin permission
  PASS  docs: README and QUICKSTART say PHP 8.1+, and the README names the Single or Extended licence
  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: a paused webhook is sent nothing
  PASS  webhooks: supplier.created is sent by the browser action where it happens [302]
  PASS  webhooks: contract.created is sent by the browser action where it happens [302]
  PASS  webhooks: contract.closed is sent by the browser action where it happens [302]
  PASS  webhooks: every event the Settings page offers is sent somewhere
  PASS  webhooks: saving an already-closed contract again does not send contract.closed twice
  PASS  backup gate: the app answered under PHP's built-in server with no PHP error in its log
  PASS  backup gate: a signed-in viewer gets 403 on /backup.json and /backup.sqlite, and nothing is served
  PASS  backup gate: a signed-in member gets 403 on /backup.json and /backup.sqlite, and nothing is served
  PASS  backup gate: the administrator gets both backups (JSON and the SQLite file)
  PASS  backup gate: the JSON backup leaves out the 2FA seed, SMTP password, webhook secret and SSO secret
  PASS  backup gate: the scheduled-backup route refuses a missing and a wrong token
  PASS  backup gate: a signed-in viewer gets 403 on the restore page and on POST /restore, /restore/commit, /api-settings/backup-token [{"page":403,"posts":{"\/restore":403,"\/restore\/commit":403,"\/api-settings\/backup-token":403},"csrf":true}]
  PASS  backup gate: a signed-in viewer without the token is refused by the scheduled-backup route
  PASS  backup gate: a signed-in member gets 403 on the restore page and on POST /restore, /restore/commit, /api-settings/backup-token [{"page":403,"posts":{"\/restore":403,"\/restore\/commit":403,"\/api-settings\/backup-token":403},"csrf":true}]
  PASS  backup gate: a signed-in member without the token is refused by the scheduled-backup route
  PASS  backup gate: the administrator opens the restore page and is not refused on any restore or backup-token route [{"page":200,"posts":{"\/restore":302,"\/restore\/commit":302,"\/api-settings\/backup-token":302}}]
  PASS  backup gate: the JSON backup carries no reusable credential from any table (10 columns: api_keys.token_hash, feed_tokens.token_hash, invites.token_hash, settings.smtp_pass, settings.oidc_client_secret, settings.backup_token_hash, users.password_hash, users.totp_secret, users.totp_recovery, webhooks.secret)
  PASS  backup gate: each planted credential row is in the backup with the cell redacted, not dropped
  PASS  links: https behind a trusted TLS proxy (X-Forwarded-Proto or CF-Visitor); a direct client cannot claim it; plain http stays http
  PASS  links: nothing builds a scheme from $_SERVER[HTTPS] alone any more (src, controllers, views)
  PASS  demo ribbon: every page carries the admin wording (the product has no public page)
  PASS  demo ribbon: below 600px it starts as the small "Own it" pill; a tap opens the card, and that choice holds for the session
  PASS  demo ribbon: injected just before </body>; output with no </body> (JSON, CSV, PDF) passes through untouched

————————————————————————————————————————————————————
ALL 360 TESTS PASSED (0 failed)

← Back to Supplia · 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 →