@remnic/core 9.54.4 → 9.54.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/access-admin-ops-surface.d.ts +24 -23
- package/dist/access-admin-ops-surface.js +55 -50
- package/dist/access-authorization-probe.d.ts +25 -22
- package/dist/access-authorization-probe.js +58 -51
- package/dist/access-boundary.d.ts +26 -23
- package/dist/access-boundary.js +59 -52
- package/dist/access-cli.js +115 -107
- package/dist/access-cli.js.map +1 -1
- package/dist/access-coding-context-resolution.d.ts +1 -1
- package/dist/access-extraction-force-flush.d.ts +19 -18
- package/dist/access-extraction-force-flush.js +55 -50
- package/dist/access-health-types.d.ts +1 -1
- package/dist/access-http-lcm-compaction.d.ts +22 -21
- package/dist/access-http-lifecycle-flush.d.ts +22 -21
- package/dist/access-http-offline-stream.d.ts +19 -18
- package/dist/access-http-query.js +55 -50
- package/dist/access-http.d.ts +55 -36
- package/dist/access-http.js +76 -65
- package/dist/access-identity-continuity-surface.d.ts +9 -9
- package/dist/access-identity-continuity-surface.js +55 -50
- package/dist/access-lcm-surface.d.ts +21 -20
- package/dist/access-lcm-surface.js +55 -50
- package/dist/access-mcp.d.ts +22 -21
- package/dist/access-mcp.js +63 -57
- package/dist/access-memory-search-fanout.d.ts +22 -2
- package/dist/access-memory-search-fanout.js +1 -1
- package/dist/{access-namespace-preflight-C7n4tgWC.d.ts → access-namespace-preflight-CCNn_6NE.d.ts} +1 -1
- package/dist/access-namespace-preflight.d.ts +3 -3
- package/dist/access-namespace-preflight.js +55 -50
- package/dist/access-observe-write-surface.d.ts +22 -21
- package/dist/access-observe-write-surface.js +55 -50
- package/dist/access-offline-manifest.d.ts +10 -9
- package/dist/access-offline-manifest.js +12 -10
- package/dist/access-operations-batch.js +57 -52
- package/dist/access-operations.d.ts +23 -22
- package/dist/access-operations.js +61 -55
- package/dist/access-recall-concurrency.d.ts +21 -20
- package/dist/access-recall-concurrency.js +55 -50
- package/dist/access-recall-response.d.ts +19 -18
- package/dist/access-recall-response.js +55 -50
- package/dist/access-recall-surface.d.ts +19 -18
- package/dist/access-recall-surface.js +55 -50
- package/dist/access-schema.d.ts +2 -2
- package/dist/access-schema.js +5 -4
- package/dist/{access-service-CXvbxLK-.d.ts → access-service-DkSdXK4S.d.ts} +1314 -83
- package/dist/access-service-helpers.d.ts +22 -21
- package/dist/access-service.d.ts +31 -30
- package/dist/access-service.js +55 -50
- package/dist/access-surface-catalog.d.ts +22 -21
- package/dist/access-surface-catalog.js +209 -2
- package/dist/access-surface-catalog.js.map +1 -1
- package/dist/access-wearables-meetings-surface.d.ts +3 -3
- package/dist/action-confidence.d.ts +1 -1
- package/dist/active-memory-bridge.d.ts +14 -1
- package/dist/active-memory-bridge.js +6 -1
- package/dist/active-recall.d.ts +1 -1
- package/dist/active-recall.js +4 -2
- package/dist/active-recall.js.map +1 -1
- package/dist/adapters/index.js +7 -7
- package/dist/adapters/registry.js +3 -3
- package/dist/admin-console/public/what-helps-me/app.js +1594 -0
- package/dist/admin-console/public/what-helps-me/index.html +339 -0
- package/dist/admin-console/public/what-helps-me/model.js +685 -0
- package/dist/admin-console/public/what-helps-me/what-helps-me.css +1296 -0
- package/dist/ambient-provenance.d.ts +1 -1
- package/dist/artifact-search.d.ts +1 -1
- package/dist/{auto-sync-TDHLZPVP.js → auto-sync-NR3GPCOA.js} +4 -4
- package/dist/behavior-learner.d.ts +1 -1
- package/dist/behavior-signals.d.ts +1 -1
- package/dist/bootstrap.d.ts +9 -9
- package/dist/briefing.d.ts +2 -2
- package/dist/briefing.js +33 -12
- package/dist/buffer-surprise-report.d.ts +1 -1
- package/dist/buffer-turn-helpers.d.ts +1 -1
- package/dist/buffer.d.ts +2 -2
- package/dist/buffer.js +3 -3
- package/dist/bulk-import/index.d.ts +3 -3
- package/dist/calibration.d.ts +1 -1
- package/dist/calibration.js +1 -1
- package/dist/capabilities.d.ts +1 -1
- package/dist/{catalog-BOOxl-I2.d.ts → catalog-DGFf8mD9.d.ts} +1 -1
- package/dist/causal-behavior.d.ts +1 -1
- package/dist/causal-consolidation.d.ts +1 -1
- package/dist/causal-consolidation.js +34 -13
- package/dist/causal-consolidation.js.map +1 -1
- package/dist/causal-trajectory-graph.d.ts +1 -1
- package/dist/{chunk-ZYLC7NFO.js → chunk-26ZXBTYN.js} +4 -4
- package/dist/chunk-26ZXBTYN.js.map +1 -0
- package/dist/{chunk-LLJMAULE.js → chunk-2FKEIPIT.js} +5 -5
- package/dist/{chunk-3CSX3ETE.js → chunk-2LWQSV6A.js} +23 -11
- package/dist/chunk-2LWQSV6A.js.map +1 -0
- package/dist/{chunk-JBPKEARU.js → chunk-2QSZNTDO.js} +7 -7
- package/dist/chunk-37DZZFFM.js +502 -0
- package/dist/chunk-37DZZFFM.js.map +1 -0
- package/dist/chunk-3HSRWJTC.js +9 -0
- package/dist/chunk-3HSRWJTC.js.map +1 -0
- package/dist/{chunk-XUAMCLY4.js → chunk-3TIZPSJC.js} +17 -13
- package/dist/chunk-3TIZPSJC.js.map +1 -0
- package/dist/{chunk-PDDTI7IC.js → chunk-43SEBY2M.js} +1 -1
- package/dist/{chunk-HWRD23DW.js → chunk-46SZBVDS.js} +3 -3
- package/dist/{chunk-6HPHEFXT.js → chunk-5FD7JMTX.js} +2 -2
- package/dist/{chunk-7BZOBA5J.js → chunk-5NB726GV.js} +70 -16
- package/dist/chunk-5NB726GV.js.map +1 -0
- package/dist/chunk-62OKGIJA.js +39 -0
- package/dist/chunk-62OKGIJA.js.map +1 -0
- package/dist/{chunk-IBKR2TFR.js → chunk-6CYHUWCO.js} +2 -2
- package/dist/{chunk-YN6ASZUG.js → chunk-6QPI2ITJ.js} +5 -5
- package/dist/{chunk-UU3G6GUP.js → chunk-76JJSXK7.js} +1 -1
- package/dist/chunk-76JJSXK7.js.map +1 -0
- package/dist/{chunk-PX2LSHYM.js → chunk-7BD6O7ZV.js} +189 -4
- package/dist/chunk-7BD6O7ZV.js.map +1 -0
- package/dist/chunk-7TBXNYLO.js +346 -0
- package/dist/chunk-7TBXNYLO.js.map +1 -0
- package/dist/{chunk-NH5IR2WM.js → chunk-BRR5EUTF.js} +6 -2
- package/dist/chunk-BRR5EUTF.js.map +1 -0
- package/dist/{chunk-NIS256NW.js → chunk-CIAI24TQ.js} +3 -3
- package/dist/{chunk-T2JHZWH6.js → chunk-D3LBIUWV.js} +11703 -7573
- package/dist/chunk-D3LBIUWV.js.map +1 -0
- package/dist/{chunk-7FGL6CGA.js → chunk-DVYRD4HY.js} +2 -2
- package/dist/{chunk-G7Z6EENO.js → chunk-EKTP7MNS.js} +4 -4
- package/dist/{chunk-S75M5ZRK.js → chunk-F2JRDPON.js} +16 -1
- package/dist/chunk-F2JRDPON.js.map +1 -0
- package/dist/{chunk-RZ4XGKOJ.js → chunk-FFRTNF3L.js} +8 -8
- package/dist/{chunk-VKXWK44Q.js → chunk-FONWBCVY.js} +3 -3
- package/dist/{chunk-DZD576ZU.js → chunk-FPLM2TKD.js} +2 -2
- package/dist/{chunk-ZPJ4QSGP.js → chunk-FTPONF6B.js} +2 -2
- package/dist/{chunk-LJS7KUPV.js → chunk-FWX7W5PM.js} +2 -2
- package/dist/{chunk-UTRO5UJU.js → chunk-G6DFSS6A.js} +5 -1
- package/dist/chunk-G6DFSS6A.js.map +1 -0
- package/dist/{chunk-6LAPLZOS.js → chunk-GJ776BC2.js} +8 -3
- package/dist/chunk-GJ776BC2.js.map +1 -0
- package/dist/{chunk-G4T7L557.js → chunk-GN3WRFJY.js} +6 -6
- package/dist/{chunk-QT2FRYI4.js → chunk-H7NAZ3UB.js} +2 -2
- package/dist/{chunk-APPFNGX6.js → chunk-HBUFQRIM.js} +2 -2
- package/dist/{chunk-UU7TRFNE.js → chunk-HGJ6NJ4E.js} +2 -2
- package/dist/{chunk-WB7JSQCP.js → chunk-HIEWDJ2O.js} +5 -2
- package/dist/{chunk-WB7JSQCP.js.map → chunk-HIEWDJ2O.js.map} +1 -1
- package/dist/{chunk-UES4BHA2.js → chunk-I3MSGFGD.js} +17 -6
- package/dist/chunk-I3MSGFGD.js.map +1 -0
- package/dist/{chunk-C4ZNGVK3.js → chunk-IKP5OYQQ.js} +2 -2
- package/dist/{chunk-KQAFEZQX.js → chunk-IPLYGWQF.js} +5 -5
- package/dist/{chunk-RDJRFDJY.js → chunk-IQ4NVWEF.js} +2 -2
- package/dist/{chunk-CGVHPCP2.js → chunk-JYG4OLWH.js} +2 -2
- package/dist/{chunk-WOPA3SGI.js → chunk-K26NWEOC.js} +8 -8
- package/dist/{chunk-K4QS7LXU.js → chunk-K2UGPTYY.js} +485 -44
- package/dist/chunk-K2UGPTYY.js.map +1 -0
- package/dist/{chunk-L6CQ7WIW.js → chunk-KFBOPMF6.js} +11 -11
- package/dist/chunk-KFBOPMF6.js.map +1 -0
- package/dist/{chunk-G7A5HLUS.js → chunk-KJH6W2I7.js} +2 -2
- package/dist/{chunk-IXEBUYO4.js → chunk-KM55SMLG.js} +11 -2
- package/dist/{chunk-IXEBUYO4.js.map → chunk-KM55SMLG.js.map} +1 -1
- package/dist/{chunk-HOIRB77A.js → chunk-L6Z5OAS4.js} +2 -2
- package/dist/chunk-LC2BDILZ.js +57 -0
- package/dist/chunk-LC2BDILZ.js.map +1 -0
- package/dist/{chunk-227UFSEQ.js → chunk-LEPOP46W.js} +4 -4
- package/dist/{chunk-6R4G7N4S.js → chunk-M4YHAODQ.js} +388 -748
- package/dist/chunk-M4YHAODQ.js.map +1 -0
- package/dist/{chunk-3JLPGJ43.js → chunk-MIH3CJPN.js} +5 -5
- package/dist/{chunk-QHFV6VNU.js → chunk-MK5VEWCR.js} +2 -2
- package/dist/chunk-NISZQW72.js +140 -0
- package/dist/chunk-NISZQW72.js.map +1 -0
- package/dist/{chunk-GUKBW4HL.js → chunk-NQMSEB5D.js} +6 -6
- package/dist/{chunk-G2LUMAUF.js → chunk-OCIP6YMD.js} +12 -5
- package/dist/chunk-OCIP6YMD.js.map +1 -0
- package/dist/{chunk-47P3RYZP.js → chunk-PG4QBPVG.js} +21 -11
- package/dist/chunk-PG4QBPVG.js.map +1 -0
- package/dist/{chunk-WRJVB3C2.js → chunk-PO7NXBG2.js} +2 -2
- package/dist/{chunk-QWGE2PUH.js → chunk-PT54WISF.js} +190 -56
- package/dist/chunk-PT54WISF.js.map +1 -0
- package/dist/{chunk-AZM4CCAC.js → chunk-QZLDRQ4I.js} +8 -8
- package/dist/{chunk-THPPCISA.js → chunk-RBII7637.js} +5 -5
- package/dist/{chunk-JDZPNCMA.js → chunk-RUNORG2P.js} +6 -3
- package/dist/chunk-RUNORG2P.js.map +1 -0
- package/dist/{chunk-JV5YUBIZ.js → chunk-RZQ6W2W4.js} +50 -44
- package/dist/chunk-RZQ6W2W4.js.map +1 -0
- package/dist/{chunk-RUMNY5DT.js → chunk-SMNYAMJD.js} +6 -2
- package/dist/chunk-SMNYAMJD.js.map +1 -0
- package/dist/{chunk-MKQBXQKU.js → chunk-TB6CBZ5V.js} +22 -15
- package/dist/chunk-TB6CBZ5V.js.map +1 -0
- package/dist/{chunk-VMMQDEN5.js → chunk-TO7ZTWJW.js} +2 -2
- package/dist/{chunk-K2TDK7GG.js → chunk-TWTUGCTY.js} +2 -2
- package/dist/{chunk-HLYR2TQL.js → chunk-UCTS3DDH.js} +2 -2
- package/dist/{chunk-JYIJNNN3.js → chunk-UG6VXIOK.js} +2 -2
- package/dist/{chunk-WM65LU67.js → chunk-UKYS3NU5.js} +2 -2
- package/dist/{chunk-WDBX6VER.js → chunk-UPLZNJKN.js} +5 -5
- package/dist/{chunk-C23BYTJV.js → chunk-UW5NPZJ2.js} +40 -12
- package/dist/chunk-UW5NPZJ2.js.map +1 -0
- package/dist/{chunk-2GFK22OZ.js → chunk-V6ERBR7U.js} +2 -2
- package/dist/{chunk-CYF6WVE7.js → chunk-W5ZKPI34.js} +139 -158
- package/dist/chunk-W5ZKPI34.js.map +1 -0
- package/dist/chunk-XX4HPFFM.js +370 -0
- package/dist/chunk-XX4HPFFM.js.map +1 -0
- package/dist/{chunk-ZMYDYNI4.js → chunk-XZHE2DEN.js} +4 -4
- package/dist/chunk-YXQEKCZM.js +223 -0
- package/dist/chunk-YXQEKCZM.js.map +1 -0
- package/dist/{chunk-GYPQL6XN.js → chunk-Z4N7SVVF.js} +35 -47
- package/dist/chunk-Z4N7SVVF.js.map +1 -0
- package/dist/{chunk-MAXVH44E.js → chunk-Z54EM5YZ.js} +7 -7
- package/dist/{chunk-YGLY6QFV.js → chunk-ZRZVLB7O.js} +2 -2
- package/dist/{chunk-4IIFL7GC.js → chunk-ZUKIUXHQ.js} +10 -10
- package/dist/{cli-DVUFIa5E.d.ts → cli-Bze8S0Qs.d.ts} +5 -5
- package/dist/cli.d.ts +22 -21
- package/dist/cli.js +90 -80
- package/dist/coding/pre-action-gate.d.ts +1 -1
- package/dist/compounding/engine.d.ts +2 -2
- package/dist/compounding/engine.js +33 -12
- package/dist/compounding/preference-consolidator.d.ts +1 -1
- package/dist/compression-optimizer.d.ts +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.js +4 -2
- package/dist/connectors/codex-materialize-runner.d.ts +1 -1
- package/dist/connectors/codex-materialize-runner.js +33 -12
- package/dist/connectors/codex-materialize.d.ts +1 -1
- package/dist/connectors/index.d.ts +1 -1
- package/dist/connectors/index.js +32 -12
- package/dist/consolidation-provenance-check.d.ts +2 -2
- package/dist/consolidation-undo.d.ts +2 -2
- package/dist/contradiction/index.d.ts +2 -2
- package/dist/contradiction/index.js +3 -1
- package/dist/contradiction-scan-JDTBXRGW.js +20 -0
- package/dist/converge-config.d.ts +1 -1
- package/dist/convergence-refresh.d.ts +2 -2
- package/dist/convergence-refresh.js +35 -14
- package/dist/conversation-index/backend.d.ts +1 -1
- package/dist/conversation-index/backend.js +2 -2
- package/dist/conversation-index/chunker.d.ts +1 -1
- package/dist/conversation-index/faiss-adapter.d.ts +1 -1
- package/dist/conversation-index/indexer.d.ts +1 -1
- package/dist/conversation-index/search.d.ts +1 -1
- package/dist/corpus-watermark.d.ts +1 -1
- package/dist/corpus-watermark.js +25 -24
- package/dist/day-summary.d.ts +1 -1
- package/dist/delinearize.d.ts +1 -1
- package/dist/dependency-propagation-config.d.ts +1 -1
- package/dist/{dependency-propagation-delivery-XzF76C9n.d.ts → dependency-propagation-delivery-CzEfB31P.d.ts} +1 -1
- package/dist/direct-answer-wiring.d.ts +1 -1
- package/dist/direct-answer.d.ts +1 -1
- package/dist/embedding-fallback.d.ts +1 -1
- package/dist/enrichment/index.d.ts +1 -1
- package/dist/entity-retrieval.d.ts +2 -2
- package/dist/entity-retrieval.js +33 -12
- package/dist/entity-schema.d.ts +1 -1
- package/dist/explicit-capture.d.ts +9 -9
- package/dist/explicit-capture.js +7 -4
- package/dist/external-wiki-access.d.ts +22 -21
- package/dist/external-wiki-access.js +56 -51
- package/dist/external-wiki-collection-registration.d.ts +1 -1
- package/dist/external-wiki-collection.d.ts +1 -1
- package/dist/external-wiki-mcp-tools.d.ts +22 -21
- package/dist/extraction-error-classification.d.ts +1 -1
- package/dist/extraction-faithfulness.d.ts +1 -1
- package/dist/extraction-faithfulness.js +2 -2
- package/dist/extraction-judge-telemetry.d.ts +1 -1
- package/dist/extraction-judge-training.d.ts +1 -1
- package/dist/extraction-judge.d.ts +1 -1
- package/dist/extraction-judge.js +2 -2
- package/dist/extraction-liveness.d.ts +1 -1
- package/dist/extraction-normalization.d.ts +1 -1
- package/dist/extraction-prompt.d.ts +1 -1
- package/dist/extraction-source-grounding-rules.d.ts +1 -1
- package/dist/extraction-source-grounding.d.ts +1 -1
- package/dist/extraction.d.ts +1 -1
- package/dist/extraction.js +35 -19
- package/dist/fallback-llm.d.ts +14 -1
- package/dist/fallback-llm.js +1 -1
- package/dist/{first-start-migration-626C5554.js → first-start-migration-CRZCZAJN.js} +11 -3
- package/dist/first-start-migration-CRZCZAJN.js.map +1 -0
- package/dist/graph-dashboard-diff.d.ts +1 -1
- package/dist/graph-dashboard-key.d.ts +1 -1
- package/dist/graph-dashboard-parser.d.ts +1 -1
- package/dist/graph-edge-reinforcement.d.ts +1 -1
- package/dist/graph-path-reconstruction.d.ts +1 -1
- package/dist/graph-path-scoring.d.ts +1 -1
- package/dist/graph-snapshot.d.ts +1 -1
- package/dist/graph.d.ts +1 -1
- package/dist/harmonic-retrieval.js +1 -1
- package/dist/importance.d.ts +1 -1
- package/dist/importers/index.d.ts +1 -1
- package/dist/in-flight-reads.d.ts +1 -1
- package/dist/index.d.ts +314 -308
- package/dist/index.js +304 -133
- package/dist/inline-script.d.ts +3 -0
- package/dist/inline-script.js +8 -0
- package/dist/inline-script.js.map +1 -0
- package/dist/intent.d.ts +1 -1
- package/dist/lcm/engine.d.ts +1 -1
- package/dist/lcm/engine.js +3 -3
- package/dist/lcm/index.d.ts +1 -1
- package/dist/lcm/index.js +6 -6
- package/dist/lcm/tools.d.ts +1 -1
- package/dist/lifecycle.d.ts +1 -1
- package/dist/live-connectors-runner.d.ts +1 -1
- package/dist/local-llm.d.ts +2 -1
- package/dist/local-llm.js +2 -2
- package/dist/local-model-endpoint.d.ts +1 -1
- package/dist/maintenance/memory-governance.d.ts +1 -1
- package/dist/maintenance/memory-governance.js +33 -12
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.d.ts +2 -2
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +33 -12
- package/dist/maintenance/rebuild-memory-projection.d.ts +2 -2
- package/dist/maintenance/rebuild-memory-projection.js +34 -13
- package/dist/{maintenance-BJzNIKBE.d.ts → maintenance-C8aRWfhq.d.ts} +3 -3
- package/dist/mcp-memory-inspector-app.d.ts +22 -21
- package/dist/mcp-read-only-tools.js +1 -1
- package/dist/memory-action-policy.d.ts +1 -1
- package/dist/memory-cache.d.ts +1 -1
- package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
- package/dist/memory-projection-mutations.js +2 -2
- package/dist/memory-projection-store.d.ts +6 -2
- package/dist/memory-projection-store.js +1 -1
- package/dist/memory-provenance.d.ts +1 -1
- package/dist/memory-snapshot.d.ts +1 -1
- package/dist/memory-worth-outcomes.d.ts +2 -2
- package/dist/models-json.d.ts +1 -1
- package/dist/namespaces/migrate.d.ts +3 -3
- package/dist/namespaces/migrate.js +24 -23
- package/dist/namespaces/principal.d.ts +1 -1
- package/dist/namespaces/search.d.ts +1 -1
- package/dist/namespaces/search.js +8 -8
- package/dist/namespaces/storage.d.ts +3 -3
- package/dist/namespaces/storage.js +33 -12
- package/dist/native-knowledge.d.ts +1 -1
- package/dist/offline-sync-file-io.d.ts +20 -0
- package/dist/offline-sync-file-io.js +18 -0
- package/dist/offline-sync-file-io.js.map +1 -0
- package/dist/offline-sync-impression-drain.d.ts +1 -1
- package/dist/offline-sync-impression-drain.js +5 -5
- package/dist/offline-sync.d.ts +8 -6
- package/dist/offline-sync.js +2 -1
- package/dist/operator-doctor-corpus.d.ts +1 -1
- package/dist/operator-doctor-corpus.js +26 -25
- package/dist/operator-doctor-replica.d.ts +1 -1
- package/dist/operator-toolkit.d.ts +3 -3
- package/dist/operator-toolkit.js +39 -36
- package/dist/orchestration/compression-guideline-coordinator.d.ts +2 -2
- package/dist/orchestration/maintenance.d.ts +4 -4
- package/dist/orchestration/maintenance.js +38 -18
- package/dist/{orchestrator-BiXgBJBQ.d.ts → orchestrator-D85CzDDY.d.ts} +11 -8
- package/dist/orchestrator.d.ts +9 -9
- package/dist/orchestrator.js +117 -109
- package/dist/patterns-cli.d.ts +1 -1
- package/dist/{pipeline-sz_dcuLg.d.ts → pipeline-DPcUifCO.d.ts} +1 -1
- package/dist/policy-runtime.d.ts +1 -1
- package/dist/proactive-contention.d.ts +1 -1
- package/dist/provenance.d.ts +1 -1
- package/dist/public-http-D-SyWNj6.d.ts +25 -0
- package/dist/{purge-533FZLS4.js → purge-GB7PZHT2.js} +2 -2
- package/dist/{qmd-eAWObK4M.d.ts → qmd-dFCHp1r0.d.ts} +1 -1
- package/dist/qmd-preflight.d.ts +2 -2
- package/dist/qmd-recall-cache.d.ts +1 -1
- package/dist/qmd.d.ts +2 -2
- package/dist/recall-concurrency-config.d.ts +1 -1
- package/dist/recall-disclosure-escalation.d.ts +1 -1
- package/dist/recall-explain-renderer.d.ts +1 -1
- package/dist/recall-explain-renderer.js +3 -3
- package/dist/recall-memory-map.d.ts +1 -1
- package/dist/recall-planner-llm.d.ts +1 -1
- package/dist/recall-planner-llm.js +1 -1
- package/dist/recall-state.d.ts +1 -1
- package/dist/recall-tag-filter.d.ts +1 -1
- package/dist/recall-timings.d.ts +1 -1
- package/dist/recall-xray-cli.d.ts +1 -1
- package/dist/recall-xray-cli.js +4 -4
- package/dist/recall-xray-renderer.d.ts +1 -1
- package/dist/recall-xray-renderer.js +3 -3
- package/dist/recall-xray.d.ts +1 -1
- package/dist/recall-xray.js +2 -2
- package/dist/reconcile/cursor.d.ts +2 -1
- package/dist/reconcile/manifest.d.ts +2 -1
- package/dist/reconcile/manifest.js +6 -5
- package/dist/reconcile/plan.d.ts +2 -1
- package/dist/reconcile/plan.js +3 -2
- package/dist/replay/normalizers/chatgpt.d.ts +1 -1
- package/dist/replay/normalizers/claude.d.ts +1 -1
- package/dist/replay/normalizers/openclaw.d.ts +1 -1
- package/dist/replay/normalizers/shared.d.ts +1 -1
- package/dist/replay/runner.d.ts +1 -1
- package/dist/replay/types.d.ts +1 -1
- package/dist/replica-divergence.d.ts +1 -1
- package/dist/replica-peers-config.d.ts +1 -1
- package/dist/resolve-auth-token.d.ts +1 -1
- package/dist/resume-bundles.js +5 -3
- package/dist/retrieval-agents.d.ts +2 -2
- package/dist/retrieval-tiers.d.ts +1 -1
- package/dist/routing/engine.d.ts +1 -1
- package/dist/routing/store.d.ts +1 -1
- package/dist/salvage-envelope.d.ts +1 -1
- package/dist/schemas.d.ts +38 -38
- package/dist/{scope-profiles-BBQsXuyj.d.ts → scope-profiles-DICK7GtN.d.ts} +1 -1
- package/dist/search/embed-helper.d.ts +1 -1
- package/dist/search/factory.d.ts +1 -1
- package/dist/search/factory.js +6 -6
- package/dist/search/index.d.ts +1 -1
- package/dist/search/index.js +12 -12
- package/dist/search/lancedb-backend.d.ts +1 -1
- package/dist/search/meilisearch-backend.d.ts +1 -1
- package/dist/search/noop-backend.d.ts +1 -1
- package/dist/search/orama-backend.d.ts +1 -1
- package/dist/search/port.d.ts +1 -1
- package/dist/search/remote-backend.d.ts +1 -1
- package/dist/{semantic-consolidation-HD8RkzVA.d.ts → semantic-consolidation-BP7NAX0N.d.ts} +1 -1
- package/dist/semantic-consolidation.d.ts +2 -2
- package/dist/semantic-consolidation.js +34 -13
- package/dist/semantic-rule-promotion.js +33 -12
- package/dist/semantic-rule-verifier.d.ts +1 -1
- package/dist/semantic-rule-verifier.js +33 -12
- package/dist/{service-DIrtHHEg.d.ts → service-DT-pJyda.d.ts} +660 -660
- package/dist/session-observer-bands.d.ts +1 -1
- package/dist/session-observer-state.d.ts +1 -1
- package/dist/shared-context/manager.d.ts +1 -1
- package/dist/signal.d.ts +1 -1
- package/dist/source-agent-qualifier.d.ts +1 -1
- package/dist/source-agent-qualifier.js +31 -15
- package/dist/{storage-DqCzXi56.d.ts → storage-qOnaJ1Lw.d.ts} +49 -13
- package/dist/storage.d.ts +2 -2
- package/dist/storage.js +33 -12
- package/dist/summarizer.d.ts +1 -1
- package/dist/summarizer.js +5 -5
- package/dist/summary-snapshot.d.ts +1 -1
- package/dist/support-passport/index.d.ts +518 -0
- package/dist/support-passport/index.js +357 -0
- package/dist/support-passport/index.js.map +1 -0
- package/dist/temporal-supersession.d.ts +2 -2
- package/dist/temporal-timeline-recall.d.ts +1 -1
- package/dist/temporal-validity.d.ts +1 -1
- package/dist/threading.d.ts +1 -1
- package/dist/tier-migration.d.ts +2 -2
- package/dist/tier-routing.d.ts +2 -2
- package/dist/tier-routing.js +7 -1
- package/dist/{tier-stats-2JCZYEF7.js → tier-stats-EXYSKSVN.js} +8 -2
- package/dist/{tier-stats-2JCZYEF7.js.map → tier-stats-EXYSKSVN.js.map} +1 -1
- package/dist/topics.d.ts +1 -1
- package/dist/transcript.d.ts +1 -1
- package/dist/transfer/autodetect.js +1 -1
- package/dist/transfer/backup.js +1 -1
- package/dist/transfer/capsule-export.js +2 -2
- package/dist/transfer/types.d.ts +36 -36
- package/dist/trust-score-stage.d.ts +1 -1
- package/dist/trust-score.d.ts +1 -1
- package/dist/{types-d_j4vC6N.d.ts → types-ed-SvS2t.d.ts} +8 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.js +1 -1
- package/dist/utility-runtime.d.ts +2 -2
- package/dist/verified-recall.js +33 -12
- package/dist/write-envelope.d.ts +1 -1
- package/dist/write-rate-limiter.d.ts +11 -5
- package/dist/write-rate-limiter.js +1 -1
- package/package.json +12 -2
- package/scripts/build-with-heap.mjs +11 -1
- package/src/access-authorization-probe.ts +20 -2
- package/src/access-boundary.ts +32 -0
- package/src/access-http.test.ts +1971 -643
- package/src/access-http.ts +81 -74
- package/src/access-mcp.ts +3 -1
- package/src/access-memory-search-fanout.ts +24 -1
- package/src/access-offline-manifest.ts +5 -0
- package/src/access-operations.ts +2 -0
- package/src/access-service-namespace.test.ts +14 -0
- package/src/access-service-observe-scope.test.ts +2 -0
- package/src/access-service-offline-file-content.test.ts +211 -0
- package/src/access-service.ts +628 -884
- package/src/access-surface-catalog.test.ts +40 -10
- package/src/access-surface-catalog.ts +15 -0
- package/src/active-memory-bridge.test.ts +151 -0
- package/src/active-memory-bridge.ts +95 -21
- package/src/briefing.test.ts +38 -0
- package/src/briefing.ts +3 -3
- package/src/cli.ts +6 -1
- package/src/config.ts +18 -9
- package/src/connectors/codex-materialize-runner.ts +2 -0
- package/src/contradiction/contradiction-scan.ts +2 -0
- package/src/contradiction/contradiction.test.ts +42 -0
- package/src/correction/correction-access-wiring.ts +8 -2
- package/src/correction/correction-storage-integration.test.ts +7 -0
- package/src/explicit-capture.test.ts +24 -0
- package/src/explicit-capture.ts +2 -0
- package/src/fallback-llm.test.ts +54 -0
- package/src/fallback-llm.ts +54 -9
- package/src/index.ts +3 -0
- package/src/inline-script.ts +6 -0
- package/src/local-llm-headers-timeout.test.ts +42 -0
- package/src/local-llm.ts +8 -8
- package/src/maintenance/first-start-migration.ts +2 -1
- package/src/maintenance/memory-governance.test.ts +25 -0
- package/src/maintenance/memory-governance.ts +4 -1
- package/src/maintenance/pattern-reinforcement.test.ts +20 -0
- package/src/maintenance/pattern-reinforcement.ts +2 -1
- package/src/maintenance/rebuild-memory-projection.ts +8 -8
- package/src/mcp-read-only-tools.ts +3 -0
- package/src/memory-projection-store.ts +25 -27
- package/src/offline-sync-file-io.ts +68 -0
- package/src/offline-sync.ts +43 -57
- package/src/orchestration/consolidation-run.ts +5 -4
- package/src/orchestration/contradiction-linking-coordinator.ts +7 -0
- package/src/orchestration/contradiction-localization.integration.test.ts +115 -0
- package/src/orchestration/lifecycle-policy-coordinator.ts +2 -1
- package/src/orchestration/qmd-result-resolver.test.ts +215 -14
- package/src/orchestration/qmd-result-resolver.ts +142 -82
- package/src/orchestration/recall-search-pipeline.test.ts +41 -0
- package/src/orchestration/recall-search-pipeline.ts +12 -0
- package/src/orchestration/semantic-consolidation-coordinator.ts +2 -1
- package/src/orchestration/tier-migration-coordinator.ts +23 -56
- package/src/orchestration/workspace-ops.test.ts +37 -0
- package/src/orchestration/workspace-ops.ts +5 -29
- package/src/orchestrator.ts +7 -8
- package/src/relay/mission-access.test.ts +11 -11
- package/src/relay/mission-ui.test.ts +3 -1
- package/src/storage/supersession-audit.ts +7 -1
- package/src/storage/supersession-side-effects.ts +17 -5
- package/src/storage/tombstone-blocked-capture-sync.ts +31 -0
- package/src/storage-contract/failure-semantics.test.ts +102 -0
- package/src/storage.ts +18 -19
- package/src/support-passport/access-http-base.ts +143 -0
- package/src/support-passport/access-http.ts +172 -0
- package/src/support-passport/access-operations.ts +272 -0
- package/src/support-passport/access-service-base.test.ts +193 -0
- package/src/support-passport/access-service-base.ts +176 -0
- package/src/support-passport/access-surface.test.ts +283 -0
- package/src/support-passport/access-surface.ts +286 -0
- package/src/support-passport/card-projection.test.ts +137 -0
- package/src/support-passport/card-projection.ts +348 -0
- package/src/support-passport/card-replacement.ts +126 -0
- package/src/support-passport/card-service-commit-forwarding.test.ts +160 -0
- package/src/support-passport/card-service.test.ts +2964 -0
- package/src/support-passport/card-service.ts +1184 -0
- package/src/support-passport/card-state.ts +158 -0
- package/src/support-passport/config-model-routing.test.ts +37 -0
- package/src/support-passport/config.test.ts +34 -0
- package/src/support-passport/config.ts +41 -0
- package/src/support-passport/contracts.test.ts +156 -0
- package/src/support-passport/contracts.ts +149 -0
- package/src/support-passport/errors.ts +30 -0
- package/src/support-passport/generated-batch.test.ts +185 -0
- package/src/support-passport/generated-batch.ts +338 -0
- package/src/support-passport/grant-contracts.ts +183 -0
- package/src/support-passport/grant-generated-batch-scope.test.ts +201 -0
- package/src/support-passport/grant-service-cache.test.ts +148 -0
- package/src/support-passport/grant-service-cancellation.test.ts +83 -0
- package/src/support-passport/grant-service-marker-cache.test.ts +62 -0
- package/src/support-passport/grant-service.test.ts +3496 -0
- package/src/support-passport/grant-service.ts +513 -0
- package/src/support-passport/grant-store-commit-hook.test.ts +123 -0
- package/src/support-passport/grant-store-order.test.ts +60 -0
- package/src/support-passport/grant-store-path-safety.test.ts +30 -0
- package/src/support-passport/grant-store-recovery.test.ts +448 -0
- package/src/support-passport/grant-store.ts +906 -0
- package/src/support-passport/index.ts +17 -0
- package/src/support-passport/mcp-tools.test.ts +222 -0
- package/src/support-passport/mcp-tools.ts +187 -0
- package/src/support-passport/model-adapter.test.ts +840 -0
- package/src/support-passport/model-adapter.ts +602 -0
- package/src/support-passport/model-audit-pinned-write.test.ts +59 -0
- package/src/support-passport/model-audit.ts +152 -0
- package/src/support-passport/model-bridge.test.ts +456 -0
- package/src/support-passport/model-bridge.ts +439 -0
- package/src/support-passport/model-contracts.ts +124 -0
- package/src/support-passport/model-service.test.ts +1731 -0
- package/src/support-passport/model-service.ts +399 -0
- package/src/support-passport/offline-sync-guard.ts +230 -0
- package/src/support-passport/owner-http.test.ts +671 -0
- package/src/support-passport/owner-lock.ts +70 -0
- package/src/support-passport/private-file.test.ts +99 -0
- package/src/support-passport/private-file.ts +498 -0
- package/src/support-passport/public-http.test.ts +476 -0
- package/src/support-passport/public-http.ts +601 -0
- package/src/support-passport/ui.test.ts +181 -0
- package/src/tier-routing.ts +28 -17
- package/src/training-export/converter.test.ts +24 -0
- package/src/training-export/converter.ts +2 -0
- package/src/types.ts +8 -1
- package/src/wearables/service.test.ts +13 -0
- package/src/wearables/service.ts +2 -0
- package/src/write-rate-limiter.test.ts +30 -6
- package/src/write-rate-limiter.ts +29 -18
- package/dist/chunk-3CSX3ETE.js.map +0 -1
- package/dist/chunk-47P3RYZP.js.map +0 -1
- package/dist/chunk-6LAPLZOS.js.map +0 -1
- package/dist/chunk-6R4G7N4S.js.map +0 -1
- package/dist/chunk-7BZOBA5J.js.map +0 -1
- package/dist/chunk-C23BYTJV.js.map +0 -1
- package/dist/chunk-CYF6WVE7.js.map +0 -1
- package/dist/chunk-G2LUMAUF.js.map +0 -1
- package/dist/chunk-GYPQL6XN.js.map +0 -1
- package/dist/chunk-JDZPNCMA.js.map +0 -1
- package/dist/chunk-JV5YUBIZ.js.map +0 -1
- package/dist/chunk-K4QS7LXU.js.map +0 -1
- package/dist/chunk-L6CQ7WIW.js.map +0 -1
- package/dist/chunk-MKQBXQKU.js.map +0 -1
- package/dist/chunk-NH5IR2WM.js.map +0 -1
- package/dist/chunk-PX2LSHYM.js.map +0 -1
- package/dist/chunk-QWGE2PUH.js.map +0 -1
- package/dist/chunk-RUMNY5DT.js.map +0 -1
- package/dist/chunk-S75M5ZRK.js.map +0 -1
- package/dist/chunk-T2JHZWH6.js.map +0 -1
- package/dist/chunk-UES4BHA2.js.map +0 -1
- package/dist/chunk-UTRO5UJU.js.map +0 -1
- package/dist/chunk-UU3G6GUP.js.map +0 -1
- package/dist/chunk-XUAMCLY4.js.map +0 -1
- package/dist/chunk-ZYLC7NFO.js.map +0 -1
- package/dist/contradiction-scan-HYLIRMJ2.js +0 -14
- package/dist/first-start-migration-626C5554.js.map +0 -1
- /package/dist/{auto-sync-TDHLZPVP.js.map → auto-sync-NR3GPCOA.js.map} +0 -0
- /package/dist/{chunk-LLJMAULE.js.map → chunk-2FKEIPIT.js.map} +0 -0
- /package/dist/{chunk-JBPKEARU.js.map → chunk-2QSZNTDO.js.map} +0 -0
- /package/dist/{chunk-PDDTI7IC.js.map → chunk-43SEBY2M.js.map} +0 -0
- /package/dist/{chunk-HWRD23DW.js.map → chunk-46SZBVDS.js.map} +0 -0
- /package/dist/{chunk-6HPHEFXT.js.map → chunk-5FD7JMTX.js.map} +0 -0
- /package/dist/{chunk-IBKR2TFR.js.map → chunk-6CYHUWCO.js.map} +0 -0
- /package/dist/{chunk-YN6ASZUG.js.map → chunk-6QPI2ITJ.js.map} +0 -0
- /package/dist/{chunk-NIS256NW.js.map → chunk-CIAI24TQ.js.map} +0 -0
- /package/dist/{chunk-7FGL6CGA.js.map → chunk-DVYRD4HY.js.map} +0 -0
- /package/dist/{chunk-G7Z6EENO.js.map → chunk-EKTP7MNS.js.map} +0 -0
- /package/dist/{chunk-RZ4XGKOJ.js.map → chunk-FFRTNF3L.js.map} +0 -0
- /package/dist/{chunk-VKXWK44Q.js.map → chunk-FONWBCVY.js.map} +0 -0
- /package/dist/{chunk-DZD576ZU.js.map → chunk-FPLM2TKD.js.map} +0 -0
- /package/dist/{chunk-ZPJ4QSGP.js.map → chunk-FTPONF6B.js.map} +0 -0
- /package/dist/{chunk-LJS7KUPV.js.map → chunk-FWX7W5PM.js.map} +0 -0
- /package/dist/{chunk-G4T7L557.js.map → chunk-GN3WRFJY.js.map} +0 -0
- /package/dist/{chunk-QT2FRYI4.js.map → chunk-H7NAZ3UB.js.map} +0 -0
- /package/dist/{chunk-APPFNGX6.js.map → chunk-HBUFQRIM.js.map} +0 -0
- /package/dist/{chunk-UU7TRFNE.js.map → chunk-HGJ6NJ4E.js.map} +0 -0
- /package/dist/{chunk-C4ZNGVK3.js.map → chunk-IKP5OYQQ.js.map} +0 -0
- /package/dist/{chunk-KQAFEZQX.js.map → chunk-IPLYGWQF.js.map} +0 -0
- /package/dist/{chunk-RDJRFDJY.js.map → chunk-IQ4NVWEF.js.map} +0 -0
- /package/dist/{chunk-CGVHPCP2.js.map → chunk-JYG4OLWH.js.map} +0 -0
- /package/dist/{chunk-WOPA3SGI.js.map → chunk-K26NWEOC.js.map} +0 -0
- /package/dist/{chunk-G7A5HLUS.js.map → chunk-KJH6W2I7.js.map} +0 -0
- /package/dist/{chunk-HOIRB77A.js.map → chunk-L6Z5OAS4.js.map} +0 -0
- /package/dist/{chunk-227UFSEQ.js.map → chunk-LEPOP46W.js.map} +0 -0
- /package/dist/{chunk-3JLPGJ43.js.map → chunk-MIH3CJPN.js.map} +0 -0
- /package/dist/{chunk-QHFV6VNU.js.map → chunk-MK5VEWCR.js.map} +0 -0
- /package/dist/{chunk-GUKBW4HL.js.map → chunk-NQMSEB5D.js.map} +0 -0
- /package/dist/{chunk-WRJVB3C2.js.map → chunk-PO7NXBG2.js.map} +0 -0
- /package/dist/{chunk-AZM4CCAC.js.map → chunk-QZLDRQ4I.js.map} +0 -0
- /package/dist/{chunk-THPPCISA.js.map → chunk-RBII7637.js.map} +0 -0
- /package/dist/{chunk-VMMQDEN5.js.map → chunk-TO7ZTWJW.js.map} +0 -0
- /package/dist/{chunk-K2TDK7GG.js.map → chunk-TWTUGCTY.js.map} +0 -0
- /package/dist/{chunk-HLYR2TQL.js.map → chunk-UCTS3DDH.js.map} +0 -0
- /package/dist/{chunk-JYIJNNN3.js.map → chunk-UG6VXIOK.js.map} +0 -0
- /package/dist/{chunk-WM65LU67.js.map → chunk-UKYS3NU5.js.map} +0 -0
- /package/dist/{chunk-WDBX6VER.js.map → chunk-UPLZNJKN.js.map} +0 -0
- /package/dist/{chunk-2GFK22OZ.js.map → chunk-V6ERBR7U.js.map} +0 -0
- /package/dist/{chunk-ZMYDYNI4.js.map → chunk-XZHE2DEN.js.map} +0 -0
- /package/dist/{chunk-MAXVH44E.js.map → chunk-Z54EM5YZ.js.map} +0 -0
- /package/dist/{chunk-YGLY6QFV.js.map → chunk-ZRZVLB7O.js.map} +0 -0
- /package/dist/{chunk-4IIFL7GC.js.map → chunk-ZUKIUXHQ.js.map} +0 -0
- /package/dist/{contradiction-scan-HYLIRMJ2.js.map → contradiction-scan-JDTBXRGW.js.map} +0 -0
- /package/dist/{purge-533FZLS4.js.map → purge-GB7PZHT2.js.map} +0 -0
|
@@ -0,0 +1,3496 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { renameSync, symlinkSync } from "node:fs";
|
|
3
|
+
import {
|
|
4
|
+
lstat,
|
|
5
|
+
mkdir,
|
|
6
|
+
mkdtemp,
|
|
7
|
+
open,
|
|
8
|
+
readFile,
|
|
9
|
+
readdir,
|
|
10
|
+
rm,
|
|
11
|
+
symlink,
|
|
12
|
+
writeFile,
|
|
13
|
+
} from "node:fs/promises";
|
|
14
|
+
import { tmpdir } from "node:os";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
import { test } from "node:test";
|
|
17
|
+
|
|
18
|
+
import { StorageManager } from "../storage.js";
|
|
19
|
+
import type { withHeldFileLock } from "../utils/serialize-mutations.js";
|
|
20
|
+
import { SupportPassportCardService } from "./card-service.js";
|
|
21
|
+
import { computeSupportPassportOwnerKey } from "./card-projection.js";
|
|
22
|
+
import { SupportPassportError } from "./errors.js";
|
|
23
|
+
import { SupportPassportGrantService } from "./grant-service.js";
|
|
24
|
+
import { SupportPassportGrantStore, syncDirectoryForDurability } from "./grant-store.js";
|
|
25
|
+
import { computeSupportPassportOwnerLockKey, supportPassportOwnerLockPath } from "./owner-lock.js";
|
|
26
|
+
import {
|
|
27
|
+
ensurePrivateDirectoryNoFollow,
|
|
28
|
+
ensurePrivateDirectoryTreeNoFollow,
|
|
29
|
+
readPrivateFileNoFollow,
|
|
30
|
+
removePrivateFilesNoFollow,
|
|
31
|
+
requirePrivateFileDescriptorRoot,
|
|
32
|
+
writePrivateFileAtomicallyNoFollow,
|
|
33
|
+
} from "./private-file.js";
|
|
34
|
+
|
|
35
|
+
async function makeSubject() {
|
|
36
|
+
StorageManager.clearAllStaticCaches();
|
|
37
|
+
const root = await mkdtemp(path.join(tmpdir(), "remnic-support-grants-"));
|
|
38
|
+
const aliceStorage = new StorageManager(path.join(root, "alice"));
|
|
39
|
+
const bobStorage = new StorageManager(path.join(root, "bob"));
|
|
40
|
+
await Promise.all([
|
|
41
|
+
aliceStorage.ensureDirectories(),
|
|
42
|
+
bobStorage.ensureDirectories(),
|
|
43
|
+
]);
|
|
44
|
+
let currentTime = Date.parse("2026-08-11T12:00:00.000Z");
|
|
45
|
+
const now = () => new Date(currentTime);
|
|
46
|
+
const resolveOwner = async (principal: string) => {
|
|
47
|
+
if (principal === "owner:alice")
|
|
48
|
+
return { principal, namespace: "alice", storage: aliceStorage };
|
|
49
|
+
if (principal === "owner:bob")
|
|
50
|
+
return { principal, namespace: "bob", storage: bobStorage };
|
|
51
|
+
throw new Error("unknown test principal");
|
|
52
|
+
};
|
|
53
|
+
const cardService = new SupportPassportCardService({ resolveOwner, now });
|
|
54
|
+
const grantStore = new SupportPassportGrantStore({
|
|
55
|
+
memoryDir: path.join(root, "shared"),
|
|
56
|
+
now,
|
|
57
|
+
});
|
|
58
|
+
const grantService = new SupportPassportGrantService({
|
|
59
|
+
grantStore,
|
|
60
|
+
resolveOwner,
|
|
61
|
+
resolveNamespace: async (namespace) => {
|
|
62
|
+
if (namespace === "alice") return aliceStorage;
|
|
63
|
+
if (namespace === "bob") return bobStorage;
|
|
64
|
+
throw new Error("unknown test namespace");
|
|
65
|
+
},
|
|
66
|
+
now,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
root,
|
|
71
|
+
aliceStorage,
|
|
72
|
+
cardService,
|
|
73
|
+
grantStore,
|
|
74
|
+
grantService,
|
|
75
|
+
now,
|
|
76
|
+
advance: (milliseconds: number) => {
|
|
77
|
+
currentTime += milliseconds;
|
|
78
|
+
},
|
|
79
|
+
cleanup: async () => {
|
|
80
|
+
StorageManager.clearAllStaticCaches();
|
|
81
|
+
await rm(root, { recursive: true, force: true });
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function createActiveCard(
|
|
87
|
+
subject: Awaited<ReturnType<typeof makeSubject>>,
|
|
88
|
+
title = "Quiet space",
|
|
89
|
+
) {
|
|
90
|
+
const draft = await subject.cardService.createManualDraft({
|
|
91
|
+
principal: "owner:alice",
|
|
92
|
+
title,
|
|
93
|
+
statement: "Offer me a quiet place and time.",
|
|
94
|
+
category: "environment",
|
|
95
|
+
reviewBy: "2026-09-01T12:00:00.000Z",
|
|
96
|
+
});
|
|
97
|
+
return await subject.cardService.approveCard({
|
|
98
|
+
principal: "owner:alice",
|
|
99
|
+
cardId: draft.cardId,
|
|
100
|
+
expectedRevision: draft.revision,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function expiryAfter(
|
|
105
|
+
subject: Awaited<ReturnType<typeof makeSubject>>,
|
|
106
|
+
milliseconds: number,
|
|
107
|
+
): string {
|
|
108
|
+
return new Date(subject.now().getTime() + milliseconds).toISOString();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
test("a grant stores only hashed credentials with private file permissions", async () => {
|
|
112
|
+
const subject = await makeSubject();
|
|
113
|
+
try {
|
|
114
|
+
const card = await createActiveCard(subject);
|
|
115
|
+
const created = await subject.grantService.createGrant({
|
|
116
|
+
principal: "owner:alice",
|
|
117
|
+
cards: [{ cardId: card.cardId, revision: card.revision }],
|
|
118
|
+
expiresAt: "2026-08-11T13:00:00.123Z",
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
assert.match(created.secret, /^[A-Za-z0-9_-]{43}$/);
|
|
122
|
+
assert.equal(created.grant.stateVersion, 1);
|
|
123
|
+
assert.equal(created.grant.expiresAt, "2026-08-11T13:00:00.123Z");
|
|
124
|
+
const filePath = path.join(
|
|
125
|
+
subject.root,
|
|
126
|
+
"shared",
|
|
127
|
+
"state",
|
|
128
|
+
"support-passport",
|
|
129
|
+
"grants",
|
|
130
|
+
`${created.grant.grantId}.json`,
|
|
131
|
+
);
|
|
132
|
+
const body = await readFile(filePath, "utf8");
|
|
133
|
+
const state = JSON.parse(body);
|
|
134
|
+
assert.equal(body.includes(created.secret), false);
|
|
135
|
+
assert.equal(body.includes("owner:alice"), false);
|
|
136
|
+
assert.equal(state.secretHash.length, 64);
|
|
137
|
+
assert.equal(state.ownerLockKey, computeSupportPassportOwnerLockKey("alice", "owner:alice"));
|
|
138
|
+
assert.equal(
|
|
139
|
+
supportPassportOwnerLockPath(subject.aliceStorage, { namespace: "alice", principal: "owner:alice" }),
|
|
140
|
+
supportPassportOwnerLockPath(subject.aliceStorage, { namespace: "alice", ownerKey: state.ownerLockKey })
|
|
141
|
+
);
|
|
142
|
+
assert.equal((await lstat(filePath)).mode & 0o777, 0o600);
|
|
143
|
+
assert.equal((await lstat(path.dirname(filePath))).mode & 0o777, 0o700);
|
|
144
|
+
} finally {
|
|
145
|
+
await subject.cleanup();
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("grant creation stops the new link when its owner request is cancelled after persistence", async () => {
|
|
150
|
+
const subject = await makeSubject();
|
|
151
|
+
const controller = new AbortController();
|
|
152
|
+
const create = subject.grantStore.create.bind(subject.grantStore);
|
|
153
|
+
subject.grantStore.create = async (...args) => {
|
|
154
|
+
const created = await create(...args);
|
|
155
|
+
controller.abort(new Error("simulated owner request timeout"));
|
|
156
|
+
return created;
|
|
157
|
+
};
|
|
158
|
+
try {
|
|
159
|
+
const card = await createActiveCard(subject);
|
|
160
|
+
await assert.rejects(
|
|
161
|
+
subject.grantService.createGrant(
|
|
162
|
+
{
|
|
163
|
+
principal: "owner:alice",
|
|
164
|
+
cards: [{ cardId: card.cardId, revision: card.revision }],
|
|
165
|
+
expiresAt: expiryAfter(subject, 3_600_000),
|
|
166
|
+
},
|
|
167
|
+
{ signal: controller.signal },
|
|
168
|
+
),
|
|
169
|
+
/simulated owner request timeout/,
|
|
170
|
+
);
|
|
171
|
+
const grants = await subject.grantService.listGrants({
|
|
172
|
+
principal: "owner:alice",
|
|
173
|
+
});
|
|
174
|
+
assert.equal(grants.length, 1);
|
|
175
|
+
assert.equal(grants[0]?.status, "revoked");
|
|
176
|
+
} finally {
|
|
177
|
+
await subject.cleanup();
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("owner grant listings read only the indexed owner grants", async () => {
|
|
182
|
+
const root = await mkdtemp(
|
|
183
|
+
path.join(tmpdir(), "remnic-support-grant-index-"),
|
|
184
|
+
);
|
|
185
|
+
try {
|
|
186
|
+
const grantIds = [
|
|
187
|
+
"00000000-0000-4000-8000-000000000001",
|
|
188
|
+
"00000000-0000-4000-8000-000000000002",
|
|
189
|
+
];
|
|
190
|
+
const now = new Date("2026-08-11T12:00:00.000Z");
|
|
191
|
+
const store = new SupportPassportGrantStore({
|
|
192
|
+
memoryDir: root,
|
|
193
|
+
makeGrantId: () =>
|
|
194
|
+
grantIds.shift() ?? "00000000-0000-4000-8000-000000000003",
|
|
195
|
+
now: () => now,
|
|
196
|
+
});
|
|
197
|
+
const common = {
|
|
198
|
+
namespace: "shared",
|
|
199
|
+
cards: [{ cardId: "card-1", revision: "a".repeat(64) }],
|
|
200
|
+
expiresAt: new Date(now.getTime() + 3_600_000).toISOString(),
|
|
201
|
+
};
|
|
202
|
+
const alice = await store.create({ ...common, principal: "owner:alice" });
|
|
203
|
+
const bob = await store.create({ ...common, principal: "owner:bob" });
|
|
204
|
+
const inspected = store as unknown as {
|
|
205
|
+
readState(grantId: string): Promise<unknown>;
|
|
206
|
+
};
|
|
207
|
+
const readState = inspected.readState.bind(store);
|
|
208
|
+
const readGrantIds: string[] = [];
|
|
209
|
+
inspected.readState = async (grantId) => {
|
|
210
|
+
readGrantIds.push(grantId);
|
|
211
|
+
return await readState(grantId);
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const listed = await store.listForOwner("shared", "owner:alice");
|
|
215
|
+
|
|
216
|
+
assert.deepEqual(
|
|
217
|
+
listed.map((state) => state.grantId),
|
|
218
|
+
[alice.state.grantId],
|
|
219
|
+
);
|
|
220
|
+
assert.deepEqual(readGrantIds, [alice.state.grantId]);
|
|
221
|
+
assert.notEqual(alice.state.grantId, bob.state.grantId);
|
|
222
|
+
} finally {
|
|
223
|
+
await rm(root, { recursive: true, force: true });
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test("owner grant listings propagate indexed grant read failures", async () => {
|
|
228
|
+
const root = await mkdtemp(
|
|
229
|
+
path.join(tmpdir(), "remnic-support-grant-list-error-"),
|
|
230
|
+
);
|
|
231
|
+
try {
|
|
232
|
+
const now = new Date("2026-08-11T12:00:00.000Z");
|
|
233
|
+
const store = new SupportPassportGrantStore({
|
|
234
|
+
memoryDir: root,
|
|
235
|
+
now: () => now,
|
|
236
|
+
});
|
|
237
|
+
const created = await store.create({
|
|
238
|
+
namespace: "alice",
|
|
239
|
+
principal: "owner:alice",
|
|
240
|
+
cards: [{ cardId: "card-1", revision: "a".repeat(64) }],
|
|
241
|
+
expiresAt: new Date(now.getTime() + 3_600_000).toISOString(),
|
|
242
|
+
});
|
|
243
|
+
const inspected = store as unknown as {
|
|
244
|
+
readState(grantId: string): Promise<unknown>;
|
|
245
|
+
};
|
|
246
|
+
inspected.readState = async (grantId) => {
|
|
247
|
+
assert.equal(grantId, created.state.grantId);
|
|
248
|
+
throw Object.assign(new Error("simulated grant read failure"), {
|
|
249
|
+
code: "EIO",
|
|
250
|
+
});
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
await assert.rejects(
|
|
254
|
+
store.listForOwner("alice", "owner:alice"),
|
|
255
|
+
(error: unknown) => (error as NodeJS.ErrnoException).code === "EIO",
|
|
256
|
+
);
|
|
257
|
+
} finally {
|
|
258
|
+
await rm(root, { recursive: true, force: true });
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test("owner grant listings put active links before inactive history", async () => {
|
|
263
|
+
const root = await mkdtemp(
|
|
264
|
+
path.join(tmpdir(), "remnic-support-grant-list-order-"),
|
|
265
|
+
);
|
|
266
|
+
try {
|
|
267
|
+
let currentTime = Date.parse("2026-08-11T12:00:00.000Z");
|
|
268
|
+
let nextGrantId = 1;
|
|
269
|
+
const store = new SupportPassportGrantStore({
|
|
270
|
+
memoryDir: root,
|
|
271
|
+
makeGrantId: () =>
|
|
272
|
+
`00000000-0000-4000-8000-${String(nextGrantId++).padStart(12, "0")}`,
|
|
273
|
+
now: () => new Date(currentTime),
|
|
274
|
+
});
|
|
275
|
+
const input = {
|
|
276
|
+
namespace: "alice",
|
|
277
|
+
principal: "owner:alice",
|
|
278
|
+
cards: [{ cardId: "card-1", revision: "a".repeat(64) }],
|
|
279
|
+
expiresAt: new Date(currentTime + 3_600_000).toISOString(),
|
|
280
|
+
};
|
|
281
|
+
const inactive = await store.create(input);
|
|
282
|
+
currentTime += 1_000;
|
|
283
|
+
const active = await store.create({
|
|
284
|
+
...input,
|
|
285
|
+
expiresAt: new Date(currentTime + 3_600_000).toISOString(),
|
|
286
|
+
});
|
|
287
|
+
await store.revoke({
|
|
288
|
+
grantId: inactive.state.grantId,
|
|
289
|
+
namespace: input.namespace,
|
|
290
|
+
principal: input.principal,
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
const listed = await store.listForOwner(input.namespace, input.principal);
|
|
294
|
+
|
|
295
|
+
assert.deepEqual(
|
|
296
|
+
listed.map((state) => state.grantId),
|
|
297
|
+
[active.state.grantId, inactive.state.grantId],
|
|
298
|
+
);
|
|
299
|
+
} finally {
|
|
300
|
+
await rm(root, { recursive: true, force: true });
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
test("owner grant operations reject a noncanonical namespace and trim the principal", async () => {
|
|
305
|
+
const root = await mkdtemp(
|
|
306
|
+
path.join(tmpdir(), "remnic-support-grant-namespace-"),
|
|
307
|
+
);
|
|
308
|
+
try {
|
|
309
|
+
const now = new Date("2026-08-11T12:00:00.000Z");
|
|
310
|
+
const store = new SupportPassportGrantStore({
|
|
311
|
+
memoryDir: root,
|
|
312
|
+
now: () => now,
|
|
313
|
+
});
|
|
314
|
+
await assert.rejects(
|
|
315
|
+
store.create({
|
|
316
|
+
namespace: " alice ",
|
|
317
|
+
principal: " owner:alice ",
|
|
318
|
+
cards: [{ cardId: "card-1", revision: "a".repeat(64) }],
|
|
319
|
+
expiresAt: new Date(now.getTime() + 3_600_000).toISOString(),
|
|
320
|
+
}),
|
|
321
|
+
(error: unknown) =>
|
|
322
|
+
error instanceof SupportPassportError && error.code === "invalid_input",
|
|
323
|
+
);
|
|
324
|
+
const created = await store.create({
|
|
325
|
+
namespace: "alice",
|
|
326
|
+
principal: " owner:alice ",
|
|
327
|
+
cards: [{ cardId: "card-1", revision: "a".repeat(64) }],
|
|
328
|
+
expiresAt: new Date(now.getTime() + 3_600_000).toISOString(),
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
assert.deepEqual(
|
|
332
|
+
(await store.listForOwner("alice", "owner:alice")).map(
|
|
333
|
+
(state) => state.grantId,
|
|
334
|
+
),
|
|
335
|
+
[created.state.grantId],
|
|
336
|
+
);
|
|
337
|
+
const revoked = await store.revoke({
|
|
338
|
+
grantId: created.state.grantId,
|
|
339
|
+
namespace: "alice",
|
|
340
|
+
principal: " owner:alice ",
|
|
341
|
+
expectedStateVersion: created.state.stateVersion,
|
|
342
|
+
});
|
|
343
|
+
assert.ok(revoked.revokedAt);
|
|
344
|
+
} finally {
|
|
345
|
+
await rm(root, { recursive: true, force: true });
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
test("owner grant history stays bounded while an inactive link frees capacity", async () => {
|
|
350
|
+
const root = await mkdtemp(
|
|
351
|
+
path.join(tmpdir(), "remnic-support-grant-history-"),
|
|
352
|
+
);
|
|
353
|
+
try {
|
|
354
|
+
const now = new Date("2026-08-11T12:00:00.000Z");
|
|
355
|
+
let nextGrantId = 1;
|
|
356
|
+
const store = new SupportPassportGrantStore({
|
|
357
|
+
memoryDir: root,
|
|
358
|
+
makeGrantId: () =>
|
|
359
|
+
`00000000-0000-4000-8000-${String(nextGrantId++).padStart(12, "0")}`,
|
|
360
|
+
now: () => now,
|
|
361
|
+
});
|
|
362
|
+
const input = {
|
|
363
|
+
namespace: "alice",
|
|
364
|
+
principal: "owner:alice",
|
|
365
|
+
cards: [{ cardId: "card-1", revision: "a".repeat(64) }],
|
|
366
|
+
expiresAt: new Date(now.getTime() + 3_600_000).toISOString(),
|
|
367
|
+
};
|
|
368
|
+
const first = await store.create(input);
|
|
369
|
+
for (let index = 1; index < 100; index += 1) await store.create(input);
|
|
370
|
+
|
|
371
|
+
await assert.rejects(
|
|
372
|
+
store.create(input),
|
|
373
|
+
(error: unknown) =>
|
|
374
|
+
error instanceof SupportPassportError && error.code === "invalid_input",
|
|
375
|
+
);
|
|
376
|
+
await store.revoke({
|
|
377
|
+
grantId: first.state.grantId,
|
|
378
|
+
namespace: input.namespace,
|
|
379
|
+
principal: input.principal,
|
|
380
|
+
expectedStateVersion: first.state.stateVersion,
|
|
381
|
+
});
|
|
382
|
+
const firstGrantPath = path.join(
|
|
383
|
+
root,
|
|
384
|
+
"state",
|
|
385
|
+
"support-passport",
|
|
386
|
+
"grants",
|
|
387
|
+
`${first.state.grantId}.json`,
|
|
388
|
+
);
|
|
389
|
+
const inspected = store as unknown as {
|
|
390
|
+
readState(grantId: string): Promise<unknown>;
|
|
391
|
+
writeOwnerIndex(ownerHash: string, grantIds: string[]): Promise<void>;
|
|
392
|
+
};
|
|
393
|
+
const readState = inspected.readState.bind(store);
|
|
394
|
+
const writeOwnerIndex = inspected.writeOwnerIndex.bind(store);
|
|
395
|
+
inspected.writeOwnerIndex = async () => {
|
|
396
|
+
throw Object.assign(new Error("simulated owner index write failure"), {
|
|
397
|
+
code: "EIO",
|
|
398
|
+
});
|
|
399
|
+
};
|
|
400
|
+
await assert.rejects(
|
|
401
|
+
store.create(input),
|
|
402
|
+
(error: unknown) => (error as NodeJS.ErrnoException).code === "EIO",
|
|
403
|
+
);
|
|
404
|
+
inspected.writeOwnerIndex = writeOwnerIndex;
|
|
405
|
+
assert.equal((await lstat(firstGrantPath)).isFile(), true);
|
|
406
|
+
assert.equal(
|
|
407
|
+
(await store.listForOwner(input.namespace, input.principal)).length,
|
|
408
|
+
100,
|
|
409
|
+
);
|
|
410
|
+
|
|
411
|
+
inspected.readState = async (grantId) => {
|
|
412
|
+
if (grantId === first.state.grantId) {
|
|
413
|
+
throw Object.assign(new Error("simulated indexed grant read failure"), {
|
|
414
|
+
code: "EIO",
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
return await readState(grantId);
|
|
418
|
+
};
|
|
419
|
+
await assert.rejects(
|
|
420
|
+
store.create(input),
|
|
421
|
+
(error: unknown) => (error as NodeJS.ErrnoException).code === "EIO",
|
|
422
|
+
);
|
|
423
|
+
inspected.readState = readState;
|
|
424
|
+
assert.equal(
|
|
425
|
+
(await store.listForOwner(input.namespace, input.principal)).length,
|
|
426
|
+
100,
|
|
427
|
+
);
|
|
428
|
+
const evictedGrantLocks: string[] = [];
|
|
429
|
+
const lockAwareStore = store as unknown as {
|
|
430
|
+
withGrantLock<T>(
|
|
431
|
+
grantId: string,
|
|
432
|
+
task: (lock: { refresh(): Promise<boolean> }) => Promise<T>,
|
|
433
|
+
): Promise<T>;
|
|
434
|
+
};
|
|
435
|
+
const withGrantLock = lockAwareStore.withGrantLock.bind(store);
|
|
436
|
+
lockAwareStore.withGrantLock = async (grantId, task) => {
|
|
437
|
+
evictedGrantLocks.push(grantId);
|
|
438
|
+
return await withGrantLock(grantId, task);
|
|
439
|
+
};
|
|
440
|
+
const replacement = await store.create(input);
|
|
441
|
+
const listed = await store.listForOwner(input.namespace, input.principal);
|
|
442
|
+
assert.equal(listed.length, 100);
|
|
443
|
+
assert.equal(
|
|
444
|
+
listed.some((state) => state.grantId === first.state.grantId),
|
|
445
|
+
false,
|
|
446
|
+
);
|
|
447
|
+
assert.equal(
|
|
448
|
+
listed.some((state) => state.grantId === replacement.state.grantId),
|
|
449
|
+
true,
|
|
450
|
+
);
|
|
451
|
+
assert.deepEqual(evictedGrantLocks, [first.state.grantId]);
|
|
452
|
+
await assert.rejects(
|
|
453
|
+
lstat(firstGrantPath),
|
|
454
|
+
(error: unknown) => (error as NodeJS.ErrnoException).code === "ENOENT",
|
|
455
|
+
);
|
|
456
|
+
} finally {
|
|
457
|
+
await rm(root, { recursive: true, force: true });
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
test("grant creation accepts a state write that committed before its durability error", async () => {
|
|
462
|
+
const root = await mkdtemp(
|
|
463
|
+
path.join(tmpdir(), "remnic-support-grant-state-commit-"),
|
|
464
|
+
);
|
|
465
|
+
try {
|
|
466
|
+
const grantId = "00000000-0000-4000-8000-000000000001";
|
|
467
|
+
const now = new Date("2026-08-11T12:00:00.000Z");
|
|
468
|
+
const directorySyncs: string[] = [];
|
|
469
|
+
const store = new SupportPassportGrantStore({
|
|
470
|
+
memoryDir: root,
|
|
471
|
+
makeGrantId: () => grantId,
|
|
472
|
+
now: () => now,
|
|
473
|
+
syncDirectory: async (directory) => {
|
|
474
|
+
directorySyncs.push(directory);
|
|
475
|
+
await syncDirectoryForDurability(directory);
|
|
476
|
+
},
|
|
477
|
+
});
|
|
478
|
+
const inspected = store as unknown as {
|
|
479
|
+
writeState(state: unknown, requireAbsent: boolean): Promise<void>;
|
|
480
|
+
};
|
|
481
|
+
const writeState = inspected.writeState.bind(store);
|
|
482
|
+
inspected.writeState = async (state, requireAbsent) => {
|
|
483
|
+
await writeState(state, requireAbsent);
|
|
484
|
+
throw Object.assign(new Error("simulated post-commit state error"), {
|
|
485
|
+
code: "EIO",
|
|
486
|
+
});
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
const created = await store.create({
|
|
490
|
+
namespace: "alice",
|
|
491
|
+
principal: "owner:alice",
|
|
492
|
+
cards: [{ cardId: "card-1", revision: "a".repeat(64) }],
|
|
493
|
+
expiresAt: new Date(now.getTime() + 3_600_000).toISOString(),
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
assert.equal(created.state.grantId, grantId);
|
|
497
|
+
assert.deepEqual(
|
|
498
|
+
(await store.listForOwner("alice", "owner:alice")).map(
|
|
499
|
+
(state) => state.grantId,
|
|
500
|
+
),
|
|
501
|
+
[grantId],
|
|
502
|
+
);
|
|
503
|
+
assert.deepEqual(
|
|
504
|
+
directorySyncs.map((directory) => path.basename(directory)),
|
|
505
|
+
["grants"]
|
|
506
|
+
);
|
|
507
|
+
} finally {
|
|
508
|
+
await rm(root, { recursive: true, force: true });
|
|
509
|
+
}
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
test("grant creation fails closed when a committed state cannot be re-synced", async () => {
|
|
513
|
+
const root = await mkdtemp(path.join(tmpdir(), "remnic-support-grant-state-sync-"));
|
|
514
|
+
try {
|
|
515
|
+
const grantId = "00000000-0000-4000-8000-000000000001";
|
|
516
|
+
const now = new Date("2026-08-11T12:00:00.000Z");
|
|
517
|
+
const store = new SupportPassportGrantStore({
|
|
518
|
+
memoryDir: root,
|
|
519
|
+
makeGrantId: () => grantId,
|
|
520
|
+
now: () => now,
|
|
521
|
+
syncDirectory: async () => {
|
|
522
|
+
throw Object.assign(new Error("simulated directory sync failure"), { code: "EIO" });
|
|
523
|
+
},
|
|
524
|
+
});
|
|
525
|
+
const inspected = store as unknown as {
|
|
526
|
+
writeState(state: unknown, requireAbsent: boolean): Promise<void>;
|
|
527
|
+
};
|
|
528
|
+
const writeState = inspected.writeState.bind(store);
|
|
529
|
+
inspected.writeState = async (state, requireAbsent) => {
|
|
530
|
+
await writeState(state, requireAbsent);
|
|
531
|
+
throw Object.assign(new Error("simulated post-commit state error"), { code: "EIO" });
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
await assert.rejects(
|
|
535
|
+
store.create({
|
|
536
|
+
namespace: "alice",
|
|
537
|
+
principal: "owner:alice",
|
|
538
|
+
cards: [{ cardId: "card-1", revision: "a".repeat(64) }],
|
|
539
|
+
expiresAt: new Date(now.getTime() + 3_600_000).toISOString(),
|
|
540
|
+
}),
|
|
541
|
+
(error: unknown) => (error as NodeJS.ErrnoException).code === "EIO"
|
|
542
|
+
);
|
|
543
|
+
await assert.rejects(
|
|
544
|
+
lstat(path.join(root, "state", "support-passport", "grants", `${grantId}.json`)),
|
|
545
|
+
(error: unknown) => (error as NodeJS.ErrnoException).code === "ENOENT"
|
|
546
|
+
);
|
|
547
|
+
} finally {
|
|
548
|
+
await rm(root, { recursive: true, force: true });
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
test("grant creation re-syncs an owner index write that committed before its durability error", async () => {
|
|
553
|
+
const root = await mkdtemp(
|
|
554
|
+
path.join(tmpdir(), "remnic-support-grant-index-commit-"),
|
|
555
|
+
);
|
|
556
|
+
try {
|
|
557
|
+
const grantId = "00000000-0000-4000-8000-000000000001";
|
|
558
|
+
const now = new Date("2026-08-11T12:00:00.000Z");
|
|
559
|
+
const store = new SupportPassportGrantStore({
|
|
560
|
+
memoryDir: root,
|
|
561
|
+
makeGrantId: () => grantId,
|
|
562
|
+
now: () => now,
|
|
563
|
+
});
|
|
564
|
+
const inspected = store as unknown as {
|
|
565
|
+
writeOwnerIndex(ownerHash: string, grantIds: string[]): Promise<void>;
|
|
566
|
+
};
|
|
567
|
+
const writeOwnerIndex = inspected.writeOwnerIndex.bind(store);
|
|
568
|
+
inspected.writeOwnerIndex = async (ownerHash, grantIds) => {
|
|
569
|
+
await writeOwnerIndex(ownerHash, grantIds);
|
|
570
|
+
throw Object.assign(new Error("simulated post-commit index error"), {
|
|
571
|
+
code: "EIO",
|
|
572
|
+
});
|
|
573
|
+
};
|
|
574
|
+
|
|
575
|
+
const created = await store.create({
|
|
576
|
+
namespace: "alice",
|
|
577
|
+
principal: "owner:alice",
|
|
578
|
+
cards: [{ cardId: "card-1", revision: "a".repeat(64) }],
|
|
579
|
+
expiresAt: new Date(now.getTime() + 3_600_000).toISOString(),
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
assert.equal(created.state.grantId, grantId);
|
|
583
|
+
assert.deepEqual(
|
|
584
|
+
(await store.listForOwner("alice", "owner:alice")).map(
|
|
585
|
+
(state) => state.grantId,
|
|
586
|
+
),
|
|
587
|
+
[grantId],
|
|
588
|
+
);
|
|
589
|
+
} finally {
|
|
590
|
+
await rm(root, { recursive: true, force: true });
|
|
591
|
+
}
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
test("idempotent revocation reports the persisted state without another sync", async () => {
|
|
595
|
+
const root = await mkdtemp(path.join(tmpdir(), "remnic-support-grant-revoke-sync-"));
|
|
596
|
+
try {
|
|
597
|
+
const now = new Date("2026-08-11T12:00:00.000Z");
|
|
598
|
+
let syncAttempts = 0;
|
|
599
|
+
const store = new SupportPassportGrantStore({
|
|
600
|
+
memoryDir: root,
|
|
601
|
+
now: () => now,
|
|
602
|
+
syncDirectory: async (directory) => {
|
|
603
|
+
syncAttempts += 1;
|
|
604
|
+
if (syncAttempts === 1) {
|
|
605
|
+
throw Object.assign(new Error("simulated directory sync failure"), { code: "EIO" });
|
|
606
|
+
}
|
|
607
|
+
await syncDirectoryForDurability(directory);
|
|
608
|
+
},
|
|
609
|
+
});
|
|
610
|
+
const created = await store.create({
|
|
611
|
+
namespace: "alice",
|
|
612
|
+
principal: "owner:alice",
|
|
613
|
+
cards: [{ cardId: "card-1", revision: "a".repeat(64) }],
|
|
614
|
+
expiresAt: new Date(now.getTime() + 3_600_000).toISOString(),
|
|
615
|
+
});
|
|
616
|
+
const inspected = store as unknown as {
|
|
617
|
+
writeState(state: { revokedAt?: string }, requireAbsent?: boolean): Promise<void>;
|
|
618
|
+
};
|
|
619
|
+
const writeState = inspected.writeState.bind(store);
|
|
620
|
+
inspected.writeState = async (state, requireAbsent) => {
|
|
621
|
+
await writeState(state, requireAbsent);
|
|
622
|
+
if (state.revokedAt) {
|
|
623
|
+
throw Object.assign(new Error("simulated post-commit state error"), { code: "EIO" });
|
|
624
|
+
}
|
|
625
|
+
};
|
|
626
|
+
const request = {
|
|
627
|
+
grantId: created.state.grantId,
|
|
628
|
+
namespace: "alice",
|
|
629
|
+
principal: "owner:alice",
|
|
630
|
+
};
|
|
631
|
+
|
|
632
|
+
await assert.rejects(store.revoke(request), (error: unknown) => (error as NodeJS.ErrnoException).code === "EIO");
|
|
633
|
+
const repeated = await store.revoke(request);
|
|
634
|
+
|
|
635
|
+
assert.ok(repeated.revokedAt);
|
|
636
|
+
assert.equal(repeated.stateVersion, 2);
|
|
637
|
+
assert.equal(syncAttempts, 1);
|
|
638
|
+
} finally {
|
|
639
|
+
await rm(root, { recursive: true, force: true });
|
|
640
|
+
}
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
test("owner grant rollover ignores a foreign derived-index entry", async () => {
|
|
644
|
+
const root = await mkdtemp(path.join(tmpdir(), "remnic-support-grant-foreign-index-"));
|
|
645
|
+
try {
|
|
646
|
+
const now = new Date("2026-08-11T12:00:00.000Z");
|
|
647
|
+
let nextGrantId = 1;
|
|
648
|
+
const store = new SupportPassportGrantStore({
|
|
649
|
+
memoryDir: root,
|
|
650
|
+
makeGrantId: () =>
|
|
651
|
+
`00000000-0000-4000-8000-${String(nextGrantId++).padStart(12, "0")}`,
|
|
652
|
+
now: () => now,
|
|
653
|
+
});
|
|
654
|
+
const common = {
|
|
655
|
+
namespace: "shared",
|
|
656
|
+
cards: [{ cardId: "card-1", revision: "a".repeat(64) }],
|
|
657
|
+
expiresAt: new Date(now.getTime() + 3_600_000).toISOString(),
|
|
658
|
+
};
|
|
659
|
+
const aliceGrants = [];
|
|
660
|
+
for (let index = 0; index < 99; index += 1) {
|
|
661
|
+
aliceGrants.push(
|
|
662
|
+
await store.create({ ...common, principal: "owner:alice" }),
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
const bob = await store.create({ ...common, principal: "owner:bob" });
|
|
666
|
+
await store.revoke({
|
|
667
|
+
grantId: bob.state.grantId,
|
|
668
|
+
namespace: common.namespace,
|
|
669
|
+
principal: "owner:bob",
|
|
670
|
+
});
|
|
671
|
+
const inspected = store as unknown as {
|
|
672
|
+
ownerHash(namespace: string, principalHash: string): string;
|
|
673
|
+
writeOwnerIndex(ownerHash: string, grantIds: string[]): Promise<void>;
|
|
674
|
+
};
|
|
675
|
+
const firstAliceGrant = aliceGrants[0];
|
|
676
|
+
assert.ok(firstAliceGrant);
|
|
677
|
+
const aliceOwnerHash = inspected.ownerHash(
|
|
678
|
+
common.namespace,
|
|
679
|
+
firstAliceGrant.state.principalHash,
|
|
680
|
+
);
|
|
681
|
+
await inspected.writeOwnerIndex(aliceOwnerHash, [
|
|
682
|
+
...aliceGrants.map((grant) => grant.state.grantId),
|
|
683
|
+
bob.state.grantId,
|
|
684
|
+
]);
|
|
685
|
+
const bobGrantPath = path.join(
|
|
686
|
+
root,
|
|
687
|
+
"state",
|
|
688
|
+
"support-passport",
|
|
689
|
+
"grants",
|
|
690
|
+
`${bob.state.grantId}.json`,
|
|
691
|
+
);
|
|
692
|
+
|
|
693
|
+
const replacement = await store.create({ ...common, principal: "owner:alice" });
|
|
694
|
+
|
|
695
|
+
assert.equal((await store.listForOwner(common.namespace, "owner:alice")).length, 100);
|
|
696
|
+
assert.equal(replacement.state.principalHash, firstAliceGrant.state.principalHash);
|
|
697
|
+
assert.equal((await lstat(bobGrantPath)).isFile(), true);
|
|
698
|
+
} finally {
|
|
699
|
+
await rm(root, { recursive: true, force: true });
|
|
700
|
+
}
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
test("owner grant history uses one expiry cutoff while it prunes old links", async () => {
|
|
704
|
+
const root = await mkdtemp(
|
|
705
|
+
path.join(tmpdir(), "remnic-support-grant-cutoff-"),
|
|
706
|
+
);
|
|
707
|
+
try {
|
|
708
|
+
const initialTime = Date.parse("2026-08-11T12:00:00.000Z");
|
|
709
|
+
let currentTime = initialTime;
|
|
710
|
+
let partitionCalls = 0;
|
|
711
|
+
let crossExpiry = false;
|
|
712
|
+
let nextGrantId = 1;
|
|
713
|
+
const store = new SupportPassportGrantStore({
|
|
714
|
+
memoryDir: root,
|
|
715
|
+
makeGrantId: () =>
|
|
716
|
+
`00000000-0000-4000-8000-${String(nextGrantId++).padStart(12, "0")}`,
|
|
717
|
+
now: () => {
|
|
718
|
+
partitionCalls += 1;
|
|
719
|
+
return new Date(
|
|
720
|
+
currentTime + (crossExpiry && partitionCalls > 101 ? 1 : 0),
|
|
721
|
+
);
|
|
722
|
+
},
|
|
723
|
+
});
|
|
724
|
+
const common = {
|
|
725
|
+
namespace: "alice",
|
|
726
|
+
principal: "owner:alice",
|
|
727
|
+
cards: [{ cardId: "card-1", revision: "a".repeat(64) }],
|
|
728
|
+
expiresAt: new Date(initialTime + 3_600_000).toISOString(),
|
|
729
|
+
};
|
|
730
|
+
const first = await store.create(common);
|
|
731
|
+
const second = await store.create(common);
|
|
732
|
+
for (let index = 2; index < 99; index += 1) await store.create(common);
|
|
733
|
+
await store.revoke({
|
|
734
|
+
grantId: first.state.grantId,
|
|
735
|
+
namespace: common.namespace,
|
|
736
|
+
principal: common.principal,
|
|
737
|
+
});
|
|
738
|
+
await store.revoke({
|
|
739
|
+
grantId: second.state.grantId,
|
|
740
|
+
namespace: common.namespace,
|
|
741
|
+
principal: common.principal,
|
|
742
|
+
});
|
|
743
|
+
currentTime = initialTime + 1_000;
|
|
744
|
+
const crossing = await store.create({
|
|
745
|
+
...common,
|
|
746
|
+
expiresAt: new Date(currentTime + 300_000).toISOString(),
|
|
747
|
+
});
|
|
748
|
+
currentTime = Date.parse(crossing.state.expiresAt) - 1;
|
|
749
|
+
partitionCalls = 0;
|
|
750
|
+
crossExpiry = true;
|
|
751
|
+
|
|
752
|
+
const created = await store.create({
|
|
753
|
+
...common,
|
|
754
|
+
expiresAt: new Date(currentTime + 3_600_000).toISOString(),
|
|
755
|
+
});
|
|
756
|
+
crossExpiry = false;
|
|
757
|
+
const listed = await store.listForOwner(common.namespace, common.principal);
|
|
758
|
+
|
|
759
|
+
assert.equal(listed.length, 100);
|
|
760
|
+
assert.equal(new Set(listed.map((state) => state.grantId)).size, 100);
|
|
761
|
+
assert.equal(
|
|
762
|
+
listed.some((state) => state.grantId === crossing.state.grantId),
|
|
763
|
+
true,
|
|
764
|
+
);
|
|
765
|
+
assert.equal(
|
|
766
|
+
listed.some((state) => state.grantId === created.state.grantId),
|
|
767
|
+
true,
|
|
768
|
+
);
|
|
769
|
+
} finally {
|
|
770
|
+
await rm(root, { recursive: true, force: true });
|
|
771
|
+
}
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
test("grant mutations report a storage conflict when the file lock is busy", async () => {
|
|
775
|
+
const root = await mkdtemp(path.join(tmpdir(), "remnic-support-grant-busy-"));
|
|
776
|
+
try {
|
|
777
|
+
const now = new Date("2026-08-11T12:00:00.000Z");
|
|
778
|
+
const rejectLock = (async (_lockPath, _options, task) =>
|
|
779
|
+
await task(false, {
|
|
780
|
+
refresh: async () => false,
|
|
781
|
+
failure: "timeout",
|
|
782
|
+
})) as typeof withHeldFileLock;
|
|
783
|
+
const store = new SupportPassportGrantStore({
|
|
784
|
+
memoryDir: root,
|
|
785
|
+
now: () => now,
|
|
786
|
+
withHeldFileLock: rejectLock,
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
await assert.rejects(
|
|
790
|
+
store.create({
|
|
791
|
+
namespace: "alice",
|
|
792
|
+
principal: "owner:alice",
|
|
793
|
+
cards: [{ cardId: "card-1", revision: "a".repeat(64) }],
|
|
794
|
+
expiresAt: new Date(now.getTime() + 300_000).toISOString(),
|
|
795
|
+
}),
|
|
796
|
+
(error: unknown) =>
|
|
797
|
+
error instanceof SupportPassportError &&
|
|
798
|
+
error.code === "storage_conflict" &&
|
|
799
|
+
error.status === 409,
|
|
800
|
+
);
|
|
801
|
+
} finally {
|
|
802
|
+
await rm(root, { recursive: true, force: true });
|
|
803
|
+
}
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
test("grant creation rechecks the owner lock immediately before commit", async () => {
|
|
807
|
+
const subject = await makeSubject();
|
|
808
|
+
try {
|
|
809
|
+
const card = await createActiveCard(subject);
|
|
810
|
+
const originalRead = subject.aliceStorage.readAllMemories.bind(
|
|
811
|
+
subject.aliceStorage,
|
|
812
|
+
);
|
|
813
|
+
let replacedLock = false;
|
|
814
|
+
subject.aliceStorage.readAllMemories = async (...args) => {
|
|
815
|
+
const memories = await originalRead(...args);
|
|
816
|
+
if (!replacedLock) {
|
|
817
|
+
replacedLock = true;
|
|
818
|
+
const lockPath = supportPassportOwnerLockPath(subject.aliceStorage, {
|
|
819
|
+
namespace: "alice",
|
|
820
|
+
principal: "owner:alice",
|
|
821
|
+
});
|
|
822
|
+
await writeFile(
|
|
823
|
+
lockPath,
|
|
824
|
+
`${process.pid} 00000000-0000-4000-8000-000000000000 peer\n`,
|
|
825
|
+
);
|
|
826
|
+
}
|
|
827
|
+
return memories;
|
|
828
|
+
};
|
|
829
|
+
|
|
830
|
+
await assert.rejects(
|
|
831
|
+
subject.grantService.createGrant({
|
|
832
|
+
principal: "owner:alice",
|
|
833
|
+
cards: [{ cardId: card.cardId, revision: card.revision }],
|
|
834
|
+
expiresAt: expiryAfter(subject, 300_000),
|
|
835
|
+
}),
|
|
836
|
+
(error: unknown) =>
|
|
837
|
+
error instanceof SupportPassportError &&
|
|
838
|
+
error.code === "storage_conflict" &&
|
|
839
|
+
error.status === 409,
|
|
840
|
+
);
|
|
841
|
+
assert.deepEqual(
|
|
842
|
+
await subject.grantService.listGrants({ principal: "owner:alice" }),
|
|
843
|
+
[],
|
|
844
|
+
);
|
|
845
|
+
} finally {
|
|
846
|
+
await subject.cleanup();
|
|
847
|
+
}
|
|
848
|
+
});
|
|
849
|
+
|
|
850
|
+
test("grant creation does not report a commit before owner indexing completes", async () => {
|
|
851
|
+
const subject = await makeSubject();
|
|
852
|
+
try {
|
|
853
|
+
const active = await createActiveCard(subject);
|
|
854
|
+
const inspected = subject.grantStore as unknown as {
|
|
855
|
+
addToOwnerIndex(): Promise<void>;
|
|
856
|
+
};
|
|
857
|
+
inspected.addToOwnerIndex = async () => {
|
|
858
|
+
throw new Error("simulated owner index failure");
|
|
859
|
+
};
|
|
860
|
+
let committed = 0;
|
|
861
|
+
|
|
862
|
+
await assert.rejects(
|
|
863
|
+
subject.grantService.createGrant(
|
|
864
|
+
{
|
|
865
|
+
principal: "owner:alice",
|
|
866
|
+
cards: [{ cardId: active.cardId, revision: active.revision }],
|
|
867
|
+
expiresAt: new Date(
|
|
868
|
+
subject.now().getTime() + 3_600_000,
|
|
869
|
+
).toISOString(),
|
|
870
|
+
},
|
|
871
|
+
{
|
|
872
|
+
onCommitted: () => {
|
|
873
|
+
committed += 1;
|
|
874
|
+
},
|
|
875
|
+
},
|
|
876
|
+
),
|
|
877
|
+
/simulated owner index failure/,
|
|
878
|
+
);
|
|
879
|
+
assert.equal(committed, 0);
|
|
880
|
+
assert.deepEqual(
|
|
881
|
+
await subject.grantService.listGrants({ principal: "owner:alice" }),
|
|
882
|
+
[],
|
|
883
|
+
);
|
|
884
|
+
} finally {
|
|
885
|
+
await subject.cleanup();
|
|
886
|
+
}
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
test("grant creation revokes its committed link when the owner lock is lost after storage", async () => {
|
|
890
|
+
const subject = await makeSubject();
|
|
891
|
+
try {
|
|
892
|
+
const card = await createActiveCard(subject);
|
|
893
|
+
const create = subject.grantStore.create.bind(subject.grantStore);
|
|
894
|
+
let committedGrantId = "";
|
|
895
|
+
let commitNotifications = 0;
|
|
896
|
+
subject.grantStore.create = async (...args) => {
|
|
897
|
+
const created = await create(...args);
|
|
898
|
+
committedGrantId = created.state.grantId;
|
|
899
|
+
const lockPath = supportPassportOwnerLockPath(subject.aliceStorage, {
|
|
900
|
+
namespace: "alice",
|
|
901
|
+
principal: "owner:alice",
|
|
902
|
+
});
|
|
903
|
+
await writeFile(lockPath, `${process.pid} 00000000-0000-4000-8000-000000000000 peer\n`);
|
|
904
|
+
return created;
|
|
905
|
+
};
|
|
906
|
+
|
|
907
|
+
await assert.rejects(
|
|
908
|
+
subject.grantService.createGrant(
|
|
909
|
+
{
|
|
910
|
+
principal: "owner:alice",
|
|
911
|
+
cards: [{ cardId: card.cardId, revision: card.revision }],
|
|
912
|
+
expiresAt: expiryAfter(subject, 3_600_000),
|
|
913
|
+
},
|
|
914
|
+
{
|
|
915
|
+
onCommitted: () => {
|
|
916
|
+
commitNotifications += 1;
|
|
917
|
+
},
|
|
918
|
+
}
|
|
919
|
+
),
|
|
920
|
+
(error: unknown) => error instanceof SupportPassportError && error.code === "storage_conflict"
|
|
921
|
+
);
|
|
922
|
+
assert.ok(committedGrantId);
|
|
923
|
+
const [stored] = await subject.grantStore.listForOwner("alice", "owner:alice");
|
|
924
|
+
assert.equal(stored?.grantId, committedGrantId);
|
|
925
|
+
assert.ok(stored?.revokedAt);
|
|
926
|
+
assert.equal(commitNotifications, 0);
|
|
927
|
+
} finally {
|
|
928
|
+
await subject.cleanup();
|
|
929
|
+
}
|
|
930
|
+
});
|
|
931
|
+
|
|
932
|
+
test("grant creation reports one commit after its final owner check", async () => {
|
|
933
|
+
const subject = await makeSubject();
|
|
934
|
+
try {
|
|
935
|
+
const card = await createActiveCard(subject);
|
|
936
|
+
let commitNotifications = 0;
|
|
937
|
+
|
|
938
|
+
const created = await subject.grantService.createGrant(
|
|
939
|
+
{
|
|
940
|
+
principal: "owner:alice",
|
|
941
|
+
cards: [{ cardId: card.cardId, revision: card.revision }],
|
|
942
|
+
expiresAt: expiryAfter(subject, 3_600_000),
|
|
943
|
+
},
|
|
944
|
+
{
|
|
945
|
+
onCommitted: () => {
|
|
946
|
+
commitNotifications += 1;
|
|
947
|
+
},
|
|
948
|
+
}
|
|
949
|
+
);
|
|
950
|
+
|
|
951
|
+
assert.equal(created.grant.status, "active");
|
|
952
|
+
assert.equal(commitNotifications, 1);
|
|
953
|
+
} finally {
|
|
954
|
+
await subject.cleanup();
|
|
955
|
+
}
|
|
956
|
+
});
|
|
957
|
+
|
|
958
|
+
test("grant revocation reports its committed state when the owner lock is lost after storage", async () => {
|
|
959
|
+
const subject = await makeSubject();
|
|
960
|
+
try {
|
|
961
|
+
const card = await createActiveCard(subject);
|
|
962
|
+
const created = await subject.grantService.createGrant({
|
|
963
|
+
principal: "owner:alice",
|
|
964
|
+
cards: [{ cardId: card.cardId, revision: card.revision }],
|
|
965
|
+
expiresAt: expiryAfter(subject, 3_600_000),
|
|
966
|
+
});
|
|
967
|
+
const revoke = subject.grantStore.revoke.bind(subject.grantStore);
|
|
968
|
+
subject.grantStore.revoke = async (...args) => {
|
|
969
|
+
const revoked = await revoke(...args);
|
|
970
|
+
const lockPath = supportPassportOwnerLockPath(subject.aliceStorage, {
|
|
971
|
+
namespace: "alice",
|
|
972
|
+
principal: "owner:alice",
|
|
973
|
+
});
|
|
974
|
+
await writeFile(lockPath, `${process.pid} 00000000-0000-4000-8000-000000000000 peer\n`);
|
|
975
|
+
return revoked;
|
|
976
|
+
};
|
|
977
|
+
let commitNotifications = 0;
|
|
978
|
+
|
|
979
|
+
const revoked = await subject.grantService.revokeGrant(
|
|
980
|
+
{
|
|
981
|
+
principal: "owner:alice",
|
|
982
|
+
grantId: created.grant.grantId,
|
|
983
|
+
expectedStateVersion: created.grant.stateVersion,
|
|
984
|
+
},
|
|
985
|
+
{
|
|
986
|
+
onCommitted: () => {
|
|
987
|
+
commitNotifications += 1;
|
|
988
|
+
},
|
|
989
|
+
}
|
|
990
|
+
);
|
|
991
|
+
|
|
992
|
+
assert.equal(revoked.status, "revoked");
|
|
993
|
+
assert.ok(revoked.revokedAt);
|
|
994
|
+
assert.equal(commitNotifications, 1);
|
|
995
|
+
} finally {
|
|
996
|
+
await subject.cleanup();
|
|
997
|
+
}
|
|
998
|
+
});
|
|
999
|
+
|
|
1000
|
+
test("grant creation measures its minimum lifetime from request receipt", async () => {
|
|
1001
|
+
const subject = await makeSubject();
|
|
1002
|
+
try {
|
|
1003
|
+
const card = await createActiveCard(subject);
|
|
1004
|
+
const expiresAt = expiryAfter(subject, 300_000);
|
|
1005
|
+
const readAllMemories = subject.aliceStorage.readAllMemories.bind(
|
|
1006
|
+
subject.aliceStorage,
|
|
1007
|
+
);
|
|
1008
|
+
let delayed = false;
|
|
1009
|
+
subject.aliceStorage.readAllMemories = async (...args) => {
|
|
1010
|
+
const memories = await readAllMemories(...args);
|
|
1011
|
+
if (!delayed) {
|
|
1012
|
+
delayed = true;
|
|
1013
|
+
subject.advance(1_000);
|
|
1014
|
+
}
|
|
1015
|
+
return memories;
|
|
1016
|
+
};
|
|
1017
|
+
|
|
1018
|
+
const created = await subject.grantService.createGrant({
|
|
1019
|
+
principal: "owner:alice",
|
|
1020
|
+
cards: [{ cardId: card.cardId, revision: card.revision }],
|
|
1021
|
+
expiresAt,
|
|
1022
|
+
});
|
|
1023
|
+
|
|
1024
|
+
assert.equal(created.grant.expiresAt, expiresAt);
|
|
1025
|
+
assert.equal(created.grant.status, "active");
|
|
1026
|
+
} finally {
|
|
1027
|
+
await subject.cleanup();
|
|
1028
|
+
}
|
|
1029
|
+
});
|
|
1030
|
+
|
|
1031
|
+
test("grant creation maps a clock jump past expiry to invalid input", async () => {
|
|
1032
|
+
const root = await mkdtemp(path.join(tmpdir(), "remnic-support-grant-clock-jump-"));
|
|
1033
|
+
try {
|
|
1034
|
+
const requestedAt = new Date("2026-08-11T12:00:00.000Z");
|
|
1035
|
+
let nowCalls = 0;
|
|
1036
|
+
const store = new SupportPassportGrantStore({
|
|
1037
|
+
memoryDir: root,
|
|
1038
|
+
now: () => {
|
|
1039
|
+
nowCalls += 1;
|
|
1040
|
+
return new Date(requestedAt.getTime() + (nowCalls > 1 ? 600_000 : 0));
|
|
1041
|
+
},
|
|
1042
|
+
});
|
|
1043
|
+
|
|
1044
|
+
await assert.rejects(
|
|
1045
|
+
store.create({
|
|
1046
|
+
namespace: "alice",
|
|
1047
|
+
principal: "owner:alice",
|
|
1048
|
+
cards: [{ cardId: "card-1", revision: "a".repeat(64) }],
|
|
1049
|
+
expiresAt: new Date(requestedAt.getTime() + 300_000).toISOString(),
|
|
1050
|
+
}),
|
|
1051
|
+
(error: unknown) =>
|
|
1052
|
+
error instanceof SupportPassportError && error.code === "invalid_input" && error.status === 400
|
|
1053
|
+
);
|
|
1054
|
+
} finally {
|
|
1055
|
+
await rm(root, { recursive: true, force: true });
|
|
1056
|
+
}
|
|
1057
|
+
});
|
|
1058
|
+
|
|
1059
|
+
test("grant creation rejects a future request time that would extend the maximum lifetime", async () => {
|
|
1060
|
+
const root = await mkdtemp(path.join(tmpdir(), "remnic-support-grant-future-request-"));
|
|
1061
|
+
try {
|
|
1062
|
+
const now = new Date("2026-08-11T12:00:00.000Z");
|
|
1063
|
+
const requestedAt = new Date("2027-08-11T12:00:00.000Z");
|
|
1064
|
+
const store = new SupportPassportGrantStore({ memoryDir: root, now: () => now });
|
|
1065
|
+
|
|
1066
|
+
await assert.rejects(
|
|
1067
|
+
store.create({
|
|
1068
|
+
namespace: "alice",
|
|
1069
|
+
principal: "owner:alice",
|
|
1070
|
+
cards: [{ cardId: "card-1", revision: "a".repeat(64) }],
|
|
1071
|
+
expiresAt: new Date(requestedAt.getTime() + 300_000).toISOString(),
|
|
1072
|
+
requestedAt,
|
|
1073
|
+
}),
|
|
1074
|
+
(error: unknown) =>
|
|
1075
|
+
error instanceof SupportPassportError && error.code === "invalid_input" && error.status === 400
|
|
1076
|
+
);
|
|
1077
|
+
} finally {
|
|
1078
|
+
await rm(root, { recursive: true, force: true });
|
|
1079
|
+
}
|
|
1080
|
+
});
|
|
1081
|
+
|
|
1082
|
+
test("directory sync ignores only explicit unsupported errors", async () => {
|
|
1083
|
+
const failingOpen = (code: string) => async () => ({
|
|
1084
|
+
sync: async () => {
|
|
1085
|
+
throw Object.assign(new Error(`simulated ${code}`), { code });
|
|
1086
|
+
},
|
|
1087
|
+
close: async () => undefined,
|
|
1088
|
+
});
|
|
1089
|
+
|
|
1090
|
+
await assert.rejects(
|
|
1091
|
+
syncDirectoryForDurability("/unused", failingOpen("EIO")),
|
|
1092
|
+
(error: unknown) => (error as NodeJS.ErrnoException).code === "EIO",
|
|
1093
|
+
);
|
|
1094
|
+
await syncDirectoryForDurability("/unused", failingOpen("EINVAL"));
|
|
1095
|
+
});
|
|
1096
|
+
|
|
1097
|
+
test("grant creation aborts before replacing an owner index after lock ownership is lost", async () => {
|
|
1098
|
+
const root = await mkdtemp(
|
|
1099
|
+
path.join(tmpdir(), "remnic-support-grant-lock-loss-"),
|
|
1100
|
+
);
|
|
1101
|
+
try {
|
|
1102
|
+
const grantIds = [
|
|
1103
|
+
"00000000-0000-4000-8000-000000000001",
|
|
1104
|
+
"00000000-0000-4000-8000-000000000002",
|
|
1105
|
+
];
|
|
1106
|
+
const now = new Date("2026-08-11T12:00:00.000Z");
|
|
1107
|
+
const store = new SupportPassportGrantStore({
|
|
1108
|
+
memoryDir: root,
|
|
1109
|
+
makeGrantId: () =>
|
|
1110
|
+
grantIds.shift() ?? "00000000-0000-4000-8000-000000000003",
|
|
1111
|
+
now: () => now,
|
|
1112
|
+
});
|
|
1113
|
+
const input = {
|
|
1114
|
+
namespace: "alice",
|
|
1115
|
+
principal: "owner:alice",
|
|
1116
|
+
cards: [{ cardId: "card-1", revision: "a".repeat(64) }],
|
|
1117
|
+
expiresAt: new Date(now.getTime() + 3_600_000).toISOString(),
|
|
1118
|
+
};
|
|
1119
|
+
const first = await store.create(input);
|
|
1120
|
+
let refreshes = 0;
|
|
1121
|
+
const inspected = store as unknown as {
|
|
1122
|
+
withMutationLock<T>(
|
|
1123
|
+
task: (lock: { refresh(): Promise<boolean> }) => Promise<T>,
|
|
1124
|
+
): Promise<T>;
|
|
1125
|
+
};
|
|
1126
|
+
inspected.withMutationLock = async (task) =>
|
|
1127
|
+
await task({
|
|
1128
|
+
refresh: async () => {
|
|
1129
|
+
refreshes += 1;
|
|
1130
|
+
return refreshes === 1;
|
|
1131
|
+
},
|
|
1132
|
+
});
|
|
1133
|
+
|
|
1134
|
+
await assert.rejects(
|
|
1135
|
+
store.create(input),
|
|
1136
|
+
(error: unknown) =>
|
|
1137
|
+
error instanceof SupportPassportError &&
|
|
1138
|
+
error.code === "storage_conflict",
|
|
1139
|
+
);
|
|
1140
|
+
assert.equal(refreshes, 2);
|
|
1141
|
+
assert.deepEqual(
|
|
1142
|
+
(await store.listForOwner("alice", "owner:alice")).map(
|
|
1143
|
+
(state) => state.grantId,
|
|
1144
|
+
),
|
|
1145
|
+
[first.state.grantId],
|
|
1146
|
+
);
|
|
1147
|
+
} finally {
|
|
1148
|
+
await rm(root, { recursive: true, force: true });
|
|
1149
|
+
}
|
|
1150
|
+
});
|
|
1151
|
+
|
|
1152
|
+
test("grant creation reconciles peer state when the owner-index lock is lost after write", async () => {
|
|
1153
|
+
const root = await mkdtemp(path.join(tmpdir(), "remnic-support-grant-index-fence-"));
|
|
1154
|
+
try {
|
|
1155
|
+
const grantIds = ["00000000-0000-4000-8000-000000000001", "00000000-0000-4000-8000-000000000002"];
|
|
1156
|
+
const peerGrantId = "00000000-0000-4000-8000-000000000003";
|
|
1157
|
+
const now = new Date("2026-08-11T12:00:00.000Z");
|
|
1158
|
+
const store = new SupportPassportGrantStore({
|
|
1159
|
+
memoryDir: root,
|
|
1160
|
+
makeGrantId: () => grantIds.shift() ?? "00000000-0000-4000-8000-000000000004",
|
|
1161
|
+
now: () => now,
|
|
1162
|
+
});
|
|
1163
|
+
const input = {
|
|
1164
|
+
namespace: "alice",
|
|
1165
|
+
principal: "owner:alice",
|
|
1166
|
+
cards: [{ cardId: "card-1", revision: "a".repeat(64) }],
|
|
1167
|
+
expiresAt: new Date(now.getTime() + 3_600_000).toISOString(),
|
|
1168
|
+
};
|
|
1169
|
+
const first = await store.create(input);
|
|
1170
|
+
const peerState = {
|
|
1171
|
+
...first.state,
|
|
1172
|
+
grantId: peerGrantId,
|
|
1173
|
+
secretHash: "b".repeat(64),
|
|
1174
|
+
};
|
|
1175
|
+
const inspected = store as unknown as {
|
|
1176
|
+
withOwnerIndexLock<T>(ownerHash: string, task: (lock: { refresh(): Promise<boolean> }) => Promise<T>): Promise<T>;
|
|
1177
|
+
writeOwnerIndex(ownerHash: string, grantIds: string[]): Promise<void>;
|
|
1178
|
+
writeState(state: typeof peerState, requireAbsent: boolean): Promise<void>;
|
|
1179
|
+
writeOwnerMembership(state: typeof peerState): Promise<void>;
|
|
1180
|
+
};
|
|
1181
|
+
const writeOwnerIndex = inspected.writeOwnerIndex.bind(store);
|
|
1182
|
+
let ownerIndexWrites = 0;
|
|
1183
|
+
inspected.writeOwnerIndex = async (ownerHash, indexedGrantIds) => {
|
|
1184
|
+
ownerIndexWrites += 1;
|
|
1185
|
+
if (ownerIndexWrites === 2) {
|
|
1186
|
+
await inspected.writeState(peerState, true);
|
|
1187
|
+
await inspected.writeOwnerMembership(peerState);
|
|
1188
|
+
await writeOwnerIndex(ownerHash, [...indexedGrantIds, peerGrantId]);
|
|
1189
|
+
}
|
|
1190
|
+
await writeOwnerIndex(ownerHash, indexedGrantIds);
|
|
1191
|
+
};
|
|
1192
|
+
let ownerLockRun = 0;
|
|
1193
|
+
const refreshesByRun = new Map<number, number>();
|
|
1194
|
+
inspected.withOwnerIndexLock = async (_ownerHash, task) => {
|
|
1195
|
+
ownerLockRun += 1;
|
|
1196
|
+
return await task({
|
|
1197
|
+
refresh: async () => {
|
|
1198
|
+
const refreshes = (refreshesByRun.get(ownerLockRun) ?? 0) + 1;
|
|
1199
|
+
refreshesByRun.set(ownerLockRun, refreshes);
|
|
1200
|
+
return ownerLockRun > 2 || refreshes !== 2;
|
|
1201
|
+
},
|
|
1202
|
+
});
|
|
1203
|
+
};
|
|
1204
|
+
|
|
1205
|
+
const created = await store.create(input);
|
|
1206
|
+
const listedIds = new Set(
|
|
1207
|
+
(await store.listForOwner(input.namespace, input.principal)).map((state) => state.grantId)
|
|
1208
|
+
);
|
|
1209
|
+
|
|
1210
|
+
assert.equal(ownerLockRun, 3);
|
|
1211
|
+
assert.equal(refreshesByRun.get(1), 2);
|
|
1212
|
+
assert.equal(refreshesByRun.get(2), 2);
|
|
1213
|
+
assert.deepEqual(listedIds, new Set([first.state.grantId, created.state.grantId, peerGrantId]));
|
|
1214
|
+
} finally {
|
|
1215
|
+
await rm(root, { recursive: true, force: true });
|
|
1216
|
+
}
|
|
1217
|
+
});
|
|
1218
|
+
|
|
1219
|
+
test("grant recovery propagates transient reads from an owner membership", async () => {
|
|
1220
|
+
const root = await mkdtemp(path.join(tmpdir(), "remnic-support-grant-recovery-read-"));
|
|
1221
|
+
try {
|
|
1222
|
+
const grantIds = ["00000000-0000-4000-8000-000000000001", "00000000-0000-4000-8000-000000000002"];
|
|
1223
|
+
const peerGrantId = "00000000-0000-4000-8000-000000000003";
|
|
1224
|
+
const now = new Date("2026-08-11T12:00:00.000Z");
|
|
1225
|
+
const store = new SupportPassportGrantStore({
|
|
1226
|
+
memoryDir: root,
|
|
1227
|
+
makeGrantId: () => grantIds.shift() ?? "00000000-0000-4000-8000-000000000004",
|
|
1228
|
+
now: () => now,
|
|
1229
|
+
});
|
|
1230
|
+
const input = {
|
|
1231
|
+
namespace: "alice",
|
|
1232
|
+
principal: "owner:alice",
|
|
1233
|
+
cards: [{ cardId: "card-1", revision: "a".repeat(64) }],
|
|
1234
|
+
expiresAt: new Date(now.getTime() + 3_600_000).toISOString(),
|
|
1235
|
+
};
|
|
1236
|
+
const first = await store.create(input);
|
|
1237
|
+
const peerState = {
|
|
1238
|
+
...first.state,
|
|
1239
|
+
grantId: peerGrantId,
|
|
1240
|
+
secretHash: "b".repeat(64),
|
|
1241
|
+
};
|
|
1242
|
+
const inspected = store as unknown as {
|
|
1243
|
+
withOwnerIndexLock<T>(ownerHash: string, task: (lock: { refresh(): Promise<boolean> }) => Promise<T>): Promise<T>;
|
|
1244
|
+
writeOwnerIndex(ownerHash: string, indexedGrantIds: string[]): Promise<void>;
|
|
1245
|
+
writeState(state: typeof peerState, requireAbsent: boolean): Promise<void>;
|
|
1246
|
+
writeOwnerMembership(state: typeof peerState): Promise<void>;
|
|
1247
|
+
readState(grantId: string): Promise<typeof peerState>;
|
|
1248
|
+
};
|
|
1249
|
+
const writeOwnerIndex = inspected.writeOwnerIndex.bind(store);
|
|
1250
|
+
let injectPeer = true;
|
|
1251
|
+
inspected.writeOwnerIndex = async (ownerHash, indexedGrantIds) => {
|
|
1252
|
+
if (injectPeer) {
|
|
1253
|
+
injectPeer = false;
|
|
1254
|
+
await inspected.writeState(peerState, true);
|
|
1255
|
+
await inspected.writeOwnerMembership(peerState);
|
|
1256
|
+
}
|
|
1257
|
+
await writeOwnerIndex(ownerHash, indexedGrantIds);
|
|
1258
|
+
};
|
|
1259
|
+
let ownerLockRun = 0;
|
|
1260
|
+
let firstRunRefreshes = 0;
|
|
1261
|
+
inspected.withOwnerIndexLock = async (_ownerHash, task) => {
|
|
1262
|
+
ownerLockRun += 1;
|
|
1263
|
+
return await task({
|
|
1264
|
+
refresh: async () => {
|
|
1265
|
+
if (ownerLockRun !== 1) return true;
|
|
1266
|
+
firstRunRefreshes += 1;
|
|
1267
|
+
return firstRunRefreshes !== 2;
|
|
1268
|
+
},
|
|
1269
|
+
});
|
|
1270
|
+
};
|
|
1271
|
+
const readState = inspected.readState.bind(store);
|
|
1272
|
+
inspected.readState = async (grantId) => {
|
|
1273
|
+
if (ownerLockRun > 1 && grantId === peerGrantId) {
|
|
1274
|
+
throw Object.assign(new Error("simulated transient owner read"), { code: "EIO" });
|
|
1275
|
+
}
|
|
1276
|
+
return await readState(grantId);
|
|
1277
|
+
};
|
|
1278
|
+
|
|
1279
|
+
await assert.rejects(store.create(input), (error: unknown) => (error as NodeJS.ErrnoException).code === "EIO");
|
|
1280
|
+
assert.equal(ownerLockRun, 2);
|
|
1281
|
+
assert.equal(
|
|
1282
|
+
JSON.parse(await readFile(path.join(root, "state", "support-passport", "grants", `${peerGrantId}.json`), "utf8"))
|
|
1283
|
+
.grantId,
|
|
1284
|
+
peerGrantId
|
|
1285
|
+
);
|
|
1286
|
+
} finally {
|
|
1287
|
+
await rm(root, { recursive: true, force: true });
|
|
1288
|
+
}
|
|
1289
|
+
});
|
|
1290
|
+
|
|
1291
|
+
test("grant recovery stops after repeated owner-index lock loss", async () => {
|
|
1292
|
+
const root = await mkdtemp(path.join(tmpdir(), "remnic-support-grant-bounded-recovery-"));
|
|
1293
|
+
try {
|
|
1294
|
+
const now = new Date("2026-08-11T12:00:00.000Z");
|
|
1295
|
+
const store = new SupportPassportGrantStore({
|
|
1296
|
+
memoryDir: root,
|
|
1297
|
+
makeGrantId: () => "00000000-0000-4000-8000-000000000001",
|
|
1298
|
+
now: () => now,
|
|
1299
|
+
});
|
|
1300
|
+
const inspected = store as unknown as {
|
|
1301
|
+
withOwnerIndexLock<T>(ownerHash: string, task: (lock: { refresh(): Promise<boolean> }) => Promise<T>): Promise<T>;
|
|
1302
|
+
};
|
|
1303
|
+
let ownerLockRuns = 0;
|
|
1304
|
+
inspected.withOwnerIndexLock = async (_ownerHash, task) => {
|
|
1305
|
+
ownerLockRuns += 1;
|
|
1306
|
+
let refreshes = 0;
|
|
1307
|
+
return await task({
|
|
1308
|
+
refresh: async () => {
|
|
1309
|
+
refreshes += 1;
|
|
1310
|
+
return refreshes === 1;
|
|
1311
|
+
},
|
|
1312
|
+
});
|
|
1313
|
+
};
|
|
1314
|
+
|
|
1315
|
+
await assert.rejects(
|
|
1316
|
+
store.create({
|
|
1317
|
+
namespace: "alice",
|
|
1318
|
+
principal: "owner:alice",
|
|
1319
|
+
cards: [{ cardId: "card-1", revision: "a".repeat(64) }],
|
|
1320
|
+
expiresAt: new Date(now.getTime() + 3_600_000).toISOString(),
|
|
1321
|
+
}),
|
|
1322
|
+
(error: unknown) => error instanceof SupportPassportError && error.code === "storage_conflict"
|
|
1323
|
+
);
|
|
1324
|
+
assert.equal(ownerLockRuns, 4);
|
|
1325
|
+
} finally {
|
|
1326
|
+
await rm(root, { recursive: true, force: true });
|
|
1327
|
+
}
|
|
1328
|
+
});
|
|
1329
|
+
|
|
1330
|
+
test("grant recovery prunes a stale owner-index entry after lock loss", async () => {
|
|
1331
|
+
const root = await mkdtemp(path.join(tmpdir(), "remnic-support-grant-stale-index-recovery-"));
|
|
1332
|
+
try {
|
|
1333
|
+
const grantIds = ["00000000-0000-4000-8000-000000000001", "00000000-0000-4000-8000-000000000002"];
|
|
1334
|
+
const now = new Date("2026-08-11T12:00:00.000Z");
|
|
1335
|
+
const store = new SupportPassportGrantStore({
|
|
1336
|
+
memoryDir: root,
|
|
1337
|
+
makeGrantId: () => grantIds.shift() ?? "00000000-0000-4000-8000-000000000003",
|
|
1338
|
+
now: () => now,
|
|
1339
|
+
});
|
|
1340
|
+
const input = {
|
|
1341
|
+
namespace: "alice",
|
|
1342
|
+
principal: "owner:alice",
|
|
1343
|
+
cards: [{ cardId: "card-1", revision: "a".repeat(64) }],
|
|
1344
|
+
expiresAt: new Date(now.getTime() + 3_600_000).toISOString(),
|
|
1345
|
+
};
|
|
1346
|
+
const first = await store.create(input);
|
|
1347
|
+
const inspected = store as unknown as {
|
|
1348
|
+
withOwnerIndexLock<T>(ownerHash: string, task: (lock: { refresh(): Promise<boolean> }) => Promise<T>): Promise<T>;
|
|
1349
|
+
writeOwnerIndex(ownerHash: string, indexedGrantIds: string[]): Promise<void>;
|
|
1350
|
+
};
|
|
1351
|
+
const writeOwnerIndex = inspected.writeOwnerIndex.bind(store);
|
|
1352
|
+
let removeStaleState = true;
|
|
1353
|
+
inspected.writeOwnerIndex = async (ownerHash, indexedGrantIds) => {
|
|
1354
|
+
await writeOwnerIndex(ownerHash, indexedGrantIds);
|
|
1355
|
+
if (!removeStaleState) return;
|
|
1356
|
+
removeStaleState = false;
|
|
1357
|
+
await rm(path.join(root, "state", "support-passport", "grants", `${first.state.grantId}.json`));
|
|
1358
|
+
};
|
|
1359
|
+
let ownerLockRun = 0;
|
|
1360
|
+
let firstRunRefreshes = 0;
|
|
1361
|
+
inspected.withOwnerIndexLock = async (_ownerHash, task) => {
|
|
1362
|
+
ownerLockRun += 1;
|
|
1363
|
+
return await task({
|
|
1364
|
+
refresh: async () => {
|
|
1365
|
+
if (ownerLockRun !== 1) return true;
|
|
1366
|
+
firstRunRefreshes += 1;
|
|
1367
|
+
return firstRunRefreshes !== 2;
|
|
1368
|
+
},
|
|
1369
|
+
});
|
|
1370
|
+
};
|
|
1371
|
+
|
|
1372
|
+
const created = await store.create(input);
|
|
1373
|
+
|
|
1374
|
+
assert.equal(ownerLockRun, 2);
|
|
1375
|
+
assert.equal(firstRunRefreshes, 2);
|
|
1376
|
+
assert.deepEqual(
|
|
1377
|
+
(await store.listForOwner(input.namespace, input.principal)).map((state) => state.grantId),
|
|
1378
|
+
[created.state.grantId]
|
|
1379
|
+
);
|
|
1380
|
+
assert.equal((await store.authenticate(created.state.grantId, created.secret)).grantId, created.state.grantId);
|
|
1381
|
+
} finally {
|
|
1382
|
+
await rm(root, { recursive: true, force: true });
|
|
1383
|
+
}
|
|
1384
|
+
});
|
|
1385
|
+
|
|
1386
|
+
test("grant recovery ignores malformed unindexed grant files", async () => {
|
|
1387
|
+
const root = await mkdtemp(path.join(tmpdir(), "remnic-support-grant-scoped-recovery-"));
|
|
1388
|
+
try {
|
|
1389
|
+
const grantIds = ["00000000-0000-4000-8000-000000000001", "00000000-0000-4000-8000-000000000002"];
|
|
1390
|
+
const unindexedGrantId = "00000000-0000-4000-8000-000000000003";
|
|
1391
|
+
const now = new Date("2026-08-11T12:00:00.000Z");
|
|
1392
|
+
const store = new SupportPassportGrantStore({
|
|
1393
|
+
memoryDir: root,
|
|
1394
|
+
makeGrantId: () => grantIds.shift() ?? "00000000-0000-4000-8000-000000000004",
|
|
1395
|
+
now: () => now,
|
|
1396
|
+
});
|
|
1397
|
+
const input = {
|
|
1398
|
+
namespace: "alice",
|
|
1399
|
+
principal: "owner:alice",
|
|
1400
|
+
cards: [{ cardId: "card-1", revision: "a".repeat(64) }],
|
|
1401
|
+
expiresAt: new Date(now.getTime() + 3_600_000).toISOString(),
|
|
1402
|
+
};
|
|
1403
|
+
const first = await store.create(input);
|
|
1404
|
+
const grantsDir = path.join(root, "state", "support-passport", "grants");
|
|
1405
|
+
await writeFile(path.join(grantsDir, `${unindexedGrantId}.json`), "not valid JSON", {
|
|
1406
|
+
mode: 0o600,
|
|
1407
|
+
});
|
|
1408
|
+
let ownerLockRun = 0;
|
|
1409
|
+
let firstRunRefreshes = 0;
|
|
1410
|
+
const inspected = store as unknown as {
|
|
1411
|
+
withOwnerIndexLock<T>(ownerHash: string, task: (lock: { refresh(): Promise<boolean> }) => Promise<T>): Promise<T>;
|
|
1412
|
+
ownerHash(namespace: string, principalHash: string): string;
|
|
1413
|
+
};
|
|
1414
|
+
const ownerHash = inspected.ownerHash(first.state.namespace, first.state.principalHash);
|
|
1415
|
+
await writeFile(
|
|
1416
|
+
path.join(grantsDir, "owners", ownerHash, `${unindexedGrantId}.json`),
|
|
1417
|
+
`${JSON.stringify({ schemaVersion: 1, grantId: unindexedGrantId })}\n`,
|
|
1418
|
+
{ mode: 0o600 }
|
|
1419
|
+
);
|
|
1420
|
+
inspected.withOwnerIndexLock = async (_ownerHash, task) => {
|
|
1421
|
+
ownerLockRun += 1;
|
|
1422
|
+
return await task({
|
|
1423
|
+
refresh: async () => {
|
|
1424
|
+
if (ownerLockRun !== 1) return true;
|
|
1425
|
+
firstRunRefreshes += 1;
|
|
1426
|
+
return firstRunRefreshes !== 2;
|
|
1427
|
+
},
|
|
1428
|
+
});
|
|
1429
|
+
};
|
|
1430
|
+
|
|
1431
|
+
const created = await store.create(input);
|
|
1432
|
+
|
|
1433
|
+
assert.equal(ownerLockRun, 2);
|
|
1434
|
+
assert.deepEqual(
|
|
1435
|
+
new Set((await store.listForOwner(input.namespace, input.principal)).map((state) => state.grantId)),
|
|
1436
|
+
new Set([first.state.grantId, created.state.grantId])
|
|
1437
|
+
);
|
|
1438
|
+
assert.equal(await readFile(path.join(grantsDir, `${unindexedGrantId}.json`), "utf8"), "not valid JSON");
|
|
1439
|
+
} finally {
|
|
1440
|
+
await rm(root, { recursive: true, force: true });
|
|
1441
|
+
}
|
|
1442
|
+
});
|
|
1443
|
+
|
|
1444
|
+
test("grant recovery reads only the affected owner's membership after lock loss", async () => {
|
|
1445
|
+
const root = await mkdtemp(path.join(tmpdir(), "remnic-support-grant-owner-scoped-recovery-"));
|
|
1446
|
+
try {
|
|
1447
|
+
const grantIds = Array.from(
|
|
1448
|
+
{ length: 8 },
|
|
1449
|
+
(_, index) => `00000000-0000-4000-8000-${String(index + 1).padStart(12, "0")}`
|
|
1450
|
+
);
|
|
1451
|
+
const now = new Date("2026-08-11T12:00:00.000Z");
|
|
1452
|
+
const store = new SupportPassportGrantStore({
|
|
1453
|
+
memoryDir: root,
|
|
1454
|
+
makeGrantId: () => grantIds.shift() ?? "00000000-0000-4000-8000-000000000099",
|
|
1455
|
+
now: () => now,
|
|
1456
|
+
});
|
|
1457
|
+
const input = {
|
|
1458
|
+
namespace: "shared",
|
|
1459
|
+
cards: [{ cardId: "card-1", revision: "a".repeat(64) }],
|
|
1460
|
+
expiresAt: new Date(now.getTime() + 3_600_000).toISOString(),
|
|
1461
|
+
};
|
|
1462
|
+
await store.create({ ...input, principal: "owner:alice" });
|
|
1463
|
+
const bobGrantIds = new Set<string>();
|
|
1464
|
+
for (let index = 0; index < 5; index += 1) {
|
|
1465
|
+
bobGrantIds.add((await store.create({ ...input, principal: "owner:bob" })).state.grantId);
|
|
1466
|
+
}
|
|
1467
|
+
const inspected = store as unknown as {
|
|
1468
|
+
withOwnerIndexLock<T>(ownerHash: string, task: (lock: { refresh(): Promise<boolean> }) => Promise<T>): Promise<T>;
|
|
1469
|
+
readState(grantId: string): Promise<{ grantId: string }>;
|
|
1470
|
+
};
|
|
1471
|
+
let ownerLockRun = 0;
|
|
1472
|
+
let firstRunRefreshes = 0;
|
|
1473
|
+
inspected.withOwnerIndexLock = async (_ownerHash, task) => {
|
|
1474
|
+
ownerLockRun += 1;
|
|
1475
|
+
return await task({
|
|
1476
|
+
refresh: async () => {
|
|
1477
|
+
if (ownerLockRun !== 1) return true;
|
|
1478
|
+
firstRunRefreshes += 1;
|
|
1479
|
+
return firstRunRefreshes !== 2;
|
|
1480
|
+
},
|
|
1481
|
+
});
|
|
1482
|
+
};
|
|
1483
|
+
const readState = inspected.readState.bind(store);
|
|
1484
|
+
const recoveryReads: string[] = [];
|
|
1485
|
+
inspected.readState = async (grantId) => {
|
|
1486
|
+
if (ownerLockRun === 2) recoveryReads.push(grantId);
|
|
1487
|
+
return await readState(grantId);
|
|
1488
|
+
};
|
|
1489
|
+
|
|
1490
|
+
await store.create({ ...input, principal: "owner:alice" });
|
|
1491
|
+
|
|
1492
|
+
assert.equal(ownerLockRun, 2);
|
|
1493
|
+
assert.equal(
|
|
1494
|
+
recoveryReads.some((grantId) => bobGrantIds.has(grantId)),
|
|
1495
|
+
false
|
|
1496
|
+
);
|
|
1497
|
+
} finally {
|
|
1498
|
+
await rm(root, { recursive: true, force: true });
|
|
1499
|
+
}
|
|
1500
|
+
});
|
|
1501
|
+
|
|
1502
|
+
test("one owner's index transaction does not block another owner", async () => {
|
|
1503
|
+
const root = await mkdtemp(path.join(tmpdir(), "remnic-support-grant-owner-concurrency-"));
|
|
1504
|
+
const releaseAlice = Promise.withResolvers<void>();
|
|
1505
|
+
try {
|
|
1506
|
+
const grantIds = ["00000000-0000-4000-8000-000000000001", "00000000-0000-4000-8000-000000000002"];
|
|
1507
|
+
const now = new Date("2026-08-11T12:00:00.000Z");
|
|
1508
|
+
const store = new SupportPassportGrantStore({
|
|
1509
|
+
memoryDir: root,
|
|
1510
|
+
makeGrantId: () => grantIds.shift() ?? "00000000-0000-4000-8000-000000000003",
|
|
1511
|
+
now: () => now,
|
|
1512
|
+
});
|
|
1513
|
+
const inspected = store as unknown as {
|
|
1514
|
+
withOwnerIndexLock<T>(ownerHash: string, task: (lock: { refresh(): Promise<boolean> }) => Promise<T>): Promise<T>;
|
|
1515
|
+
};
|
|
1516
|
+
const withOwnerIndexLock = inspected.withOwnerIndexLock.bind(store);
|
|
1517
|
+
const aliceEntered = Promise.withResolvers<void>();
|
|
1518
|
+
let blockedOwnerHash: string | undefined;
|
|
1519
|
+
inspected.withOwnerIndexLock = async (ownerHash, task) => {
|
|
1520
|
+
if (!blockedOwnerHash) {
|
|
1521
|
+
blockedOwnerHash = ownerHash;
|
|
1522
|
+
return await withOwnerIndexLock(ownerHash, async (lock) => {
|
|
1523
|
+
aliceEntered.resolve();
|
|
1524
|
+
await releaseAlice.promise;
|
|
1525
|
+
return await task(lock);
|
|
1526
|
+
});
|
|
1527
|
+
}
|
|
1528
|
+
return await withOwnerIndexLock(ownerHash, task);
|
|
1529
|
+
};
|
|
1530
|
+
const input = {
|
|
1531
|
+
namespace: "shared",
|
|
1532
|
+
cards: [{ cardId: "card-1", revision: "a".repeat(64) }],
|
|
1533
|
+
expiresAt: new Date(now.getTime() + 3_600_000).toISOString(),
|
|
1534
|
+
};
|
|
1535
|
+
const alice = store.create({ ...input, principal: "owner:alice" });
|
|
1536
|
+
await aliceEntered.promise;
|
|
1537
|
+
const bob = await Promise.race([
|
|
1538
|
+
store.create({ ...input, principal: "owner:bob" }),
|
|
1539
|
+
new Promise<never>((_resolve, reject) =>
|
|
1540
|
+
setTimeout(() => reject(new Error("another owner was blocked by Alice's index lock")), 1_000)
|
|
1541
|
+
),
|
|
1542
|
+
]);
|
|
1543
|
+
assert.equal(bob.state.namespace, "shared");
|
|
1544
|
+
releaseAlice.resolve();
|
|
1545
|
+
await alice;
|
|
1546
|
+
} finally {
|
|
1547
|
+
releaseAlice.resolve();
|
|
1548
|
+
await rm(root, { recursive: true, force: true });
|
|
1549
|
+
}
|
|
1550
|
+
});
|
|
1551
|
+
|
|
1552
|
+
test("grant creation rechecks its mutation lock after the commit callback", async () => {
|
|
1553
|
+
const root = await mkdtemp(
|
|
1554
|
+
path.join(tmpdir(), "remnic-support-grant-callback-lock-"),
|
|
1555
|
+
);
|
|
1556
|
+
try {
|
|
1557
|
+
const now = new Date("2026-08-11T12:00:00.000Z");
|
|
1558
|
+
const store = new SupportPassportGrantStore({
|
|
1559
|
+
memoryDir: root,
|
|
1560
|
+
now: () => now,
|
|
1561
|
+
});
|
|
1562
|
+
let refreshes = 0;
|
|
1563
|
+
const inspected = store as unknown as {
|
|
1564
|
+
withMutationLock<T>(
|
|
1565
|
+
task: (lock: { refresh(): Promise<boolean> }) => Promise<T>,
|
|
1566
|
+
): Promise<T>;
|
|
1567
|
+
};
|
|
1568
|
+
inspected.withMutationLock = async (task) =>
|
|
1569
|
+
await task({
|
|
1570
|
+
refresh: async () => {
|
|
1571
|
+
refreshes += 1;
|
|
1572
|
+
return refreshes === 1;
|
|
1573
|
+
},
|
|
1574
|
+
});
|
|
1575
|
+
|
|
1576
|
+
await assert.rejects(
|
|
1577
|
+
store.create(
|
|
1578
|
+
{
|
|
1579
|
+
namespace: "alice",
|
|
1580
|
+
principal: "owner:alice",
|
|
1581
|
+
cards: [{ cardId: "card-1", revision: "a".repeat(64) }],
|
|
1582
|
+
expiresAt: new Date(now.getTime() + 300_000).toISOString(),
|
|
1583
|
+
},
|
|
1584
|
+
async () => undefined,
|
|
1585
|
+
),
|
|
1586
|
+
(error: unknown) =>
|
|
1587
|
+
error instanceof SupportPassportError &&
|
|
1588
|
+
error.code === "storage_conflict",
|
|
1589
|
+
);
|
|
1590
|
+
assert.equal(refreshes, 2);
|
|
1591
|
+
assert.deepEqual(await readdir(path.join(root, "state", "support-passport", "grants")), ["owners"]);
|
|
1592
|
+
} finally {
|
|
1593
|
+
await rm(root, { recursive: true, force: true });
|
|
1594
|
+
}
|
|
1595
|
+
});
|
|
1596
|
+
|
|
1597
|
+
test("grant revocation rechecks its mutation lock after the commit callback", async () => {
|
|
1598
|
+
const root = await mkdtemp(
|
|
1599
|
+
path.join(tmpdir(), "remnic-support-revoke-callback-lock-"),
|
|
1600
|
+
);
|
|
1601
|
+
try {
|
|
1602
|
+
const now = new Date("2026-08-11T12:00:00.000Z");
|
|
1603
|
+
const store = new SupportPassportGrantStore({
|
|
1604
|
+
memoryDir: root,
|
|
1605
|
+
now: () => now,
|
|
1606
|
+
});
|
|
1607
|
+
const created = await store.create({
|
|
1608
|
+
namespace: "alice",
|
|
1609
|
+
principal: "owner:alice",
|
|
1610
|
+
cards: [{ cardId: "card-1", revision: "a".repeat(64) }],
|
|
1611
|
+
expiresAt: new Date(now.getTime() + 300_000).toISOString(),
|
|
1612
|
+
});
|
|
1613
|
+
let refreshes = 0;
|
|
1614
|
+
const inspected = store as unknown as {
|
|
1615
|
+
withGrantLock<T>(
|
|
1616
|
+
grantId: string,
|
|
1617
|
+
task: (lock: { refresh(): Promise<boolean> }) => Promise<T>,
|
|
1618
|
+
): Promise<T>;
|
|
1619
|
+
};
|
|
1620
|
+
inspected.withGrantLock = async (_grantId, task) =>
|
|
1621
|
+
await task({
|
|
1622
|
+
refresh: async () => {
|
|
1623
|
+
refreshes += 1;
|
|
1624
|
+
return refreshes === 1;
|
|
1625
|
+
},
|
|
1626
|
+
});
|
|
1627
|
+
|
|
1628
|
+
await assert.rejects(
|
|
1629
|
+
store.revoke(
|
|
1630
|
+
{
|
|
1631
|
+
grantId: created.state.grantId,
|
|
1632
|
+
namespace: "alice",
|
|
1633
|
+
principal: "owner:alice",
|
|
1634
|
+
},
|
|
1635
|
+
{ beforeCommit: async () => undefined },
|
|
1636
|
+
),
|
|
1637
|
+
(error: unknown) =>
|
|
1638
|
+
error instanceof SupportPassportError &&
|
|
1639
|
+
error.code === "storage_conflict",
|
|
1640
|
+
);
|
|
1641
|
+
assert.equal(refreshes, 2);
|
|
1642
|
+
assert.equal(
|
|
1643
|
+
(await store.authenticate(created.state.grantId, created.secret))
|
|
1644
|
+
.revokedAt,
|
|
1645
|
+
undefined,
|
|
1646
|
+
);
|
|
1647
|
+
} finally {
|
|
1648
|
+
await rm(root, { recursive: true, force: true });
|
|
1649
|
+
}
|
|
1650
|
+
});
|
|
1651
|
+
|
|
1652
|
+
test("a helper sees only the selected active card through a valid secret", async () => {
|
|
1653
|
+
const subject = await makeSubject();
|
|
1654
|
+
try {
|
|
1655
|
+
const selected = await createActiveCard(subject, "Selected card");
|
|
1656
|
+
await createActiveCard(subject, "Private card");
|
|
1657
|
+
const created = await subject.grantService.createGrant({
|
|
1658
|
+
principal: "owner:alice",
|
|
1659
|
+
cards: [{ cardId: selected.cardId, revision: selected.revision }],
|
|
1660
|
+
expiresAt: expiryAfter(subject, 3_600_000),
|
|
1661
|
+
});
|
|
1662
|
+
|
|
1663
|
+
const guide = await subject.grantService.readGrant({
|
|
1664
|
+
grantId: created.grant.grantId,
|
|
1665
|
+
secret: created.secret,
|
|
1666
|
+
});
|
|
1667
|
+
assert.deepEqual(guide.cards, [
|
|
1668
|
+
{
|
|
1669
|
+
cardId: selected.cardId,
|
|
1670
|
+
title: "Selected card",
|
|
1671
|
+
statement: "Offer me a quiet place and time.",
|
|
1672
|
+
category: "environment",
|
|
1673
|
+
updatedAt: selected.updatedAt,
|
|
1674
|
+
},
|
|
1675
|
+
]);
|
|
1676
|
+
const sharedCard = guide.cards[0];
|
|
1677
|
+
assert.ok(sharedCard);
|
|
1678
|
+
assert.equal("revision" in sharedCard, false);
|
|
1679
|
+
assert.equal("namespace" in guide, false);
|
|
1680
|
+
} finally {
|
|
1681
|
+
await subject.cleanup();
|
|
1682
|
+
}
|
|
1683
|
+
});
|
|
1684
|
+
|
|
1685
|
+
test("grants reject cards owned by another namespace in shared storage", async () => {
|
|
1686
|
+
StorageManager.clearAllStaticCaches();
|
|
1687
|
+
const root = await mkdtemp(
|
|
1688
|
+
path.join(tmpdir(), "remnic-support-grant-card-scope-"),
|
|
1689
|
+
);
|
|
1690
|
+
try {
|
|
1691
|
+
const storage = new StorageManager(path.join(root, "shared-storage"));
|
|
1692
|
+
await storage.ensureDirectories();
|
|
1693
|
+
const now = () => new Date("2026-08-11T12:00:00.000Z");
|
|
1694
|
+
const resolveOwner = async (principal: string) => ({
|
|
1695
|
+
principal,
|
|
1696
|
+
namespace: principal === "owner:alice" ? "alice" : "bob",
|
|
1697
|
+
storage,
|
|
1698
|
+
});
|
|
1699
|
+
const cardService = new SupportPassportCardService({ resolveOwner, now });
|
|
1700
|
+
const grantStore = new SupportPassportGrantStore({
|
|
1701
|
+
memoryDir: path.join(root, "grants"),
|
|
1702
|
+
now,
|
|
1703
|
+
});
|
|
1704
|
+
const grantService = new SupportPassportGrantService({
|
|
1705
|
+
grantStore,
|
|
1706
|
+
resolveOwner,
|
|
1707
|
+
resolveNamespace: async () => storage,
|
|
1708
|
+
now,
|
|
1709
|
+
});
|
|
1710
|
+
const draft = await cardService.createManualDraft({
|
|
1711
|
+
principal: "owner:alice",
|
|
1712
|
+
title: "Alice card",
|
|
1713
|
+
statement: "Offer me a quiet place.",
|
|
1714
|
+
category: "environment",
|
|
1715
|
+
reviewBy: "2026-09-01T12:00:00.000Z",
|
|
1716
|
+
});
|
|
1717
|
+
const aliceCard = await cardService.approveCard({
|
|
1718
|
+
principal: "owner:alice",
|
|
1719
|
+
cardId: draft.cardId,
|
|
1720
|
+
expectedRevision: draft.revision,
|
|
1721
|
+
});
|
|
1722
|
+
|
|
1723
|
+
await assert.rejects(
|
|
1724
|
+
grantService.createGrant({
|
|
1725
|
+
principal: "owner:bob",
|
|
1726
|
+
cards: [{ cardId: aliceCard.cardId, revision: aliceCard.revision }],
|
|
1727
|
+
expiresAt: "2026-08-11T13:00:00.000Z",
|
|
1728
|
+
}),
|
|
1729
|
+
(error: unknown) =>
|
|
1730
|
+
error instanceof SupportPassportError &&
|
|
1731
|
+
error.code === "invalid_card_status",
|
|
1732
|
+
);
|
|
1733
|
+
const forged = await grantStore.create({
|
|
1734
|
+
namespace: "bob",
|
|
1735
|
+
principal: "owner:bob",
|
|
1736
|
+
cards: [{ cardId: aliceCard.cardId, revision: aliceCard.revision }],
|
|
1737
|
+
expiresAt: "2026-08-11T13:00:00.000Z",
|
|
1738
|
+
});
|
|
1739
|
+
await assert.rejects(
|
|
1740
|
+
grantService.readGrant({
|
|
1741
|
+
grantId: forged.state.grantId,
|
|
1742
|
+
secret: forged.secret,
|
|
1743
|
+
}),
|
|
1744
|
+
(error: unknown) =>
|
|
1745
|
+
error instanceof SupportPassportError && error.code === "grant_stale",
|
|
1746
|
+
);
|
|
1747
|
+
} finally {
|
|
1748
|
+
StorageManager.clearAllStaticCaches();
|
|
1749
|
+
await rm(root, { recursive: true, force: true });
|
|
1750
|
+
}
|
|
1751
|
+
});
|
|
1752
|
+
|
|
1753
|
+
test("grants reject cards owned by another principal inside a shared namespace", async () => {
|
|
1754
|
+
StorageManager.clearAllStaticCaches();
|
|
1755
|
+
const root = await mkdtemp(
|
|
1756
|
+
path.join(tmpdir(), "remnic-support-grant-owner-scope-"),
|
|
1757
|
+
);
|
|
1758
|
+
try {
|
|
1759
|
+
const storage = new StorageManager(path.join(root, "shared-storage"));
|
|
1760
|
+
await storage.ensureDirectories();
|
|
1761
|
+
const now = () => new Date("2026-08-11T12:00:00.000Z");
|
|
1762
|
+
const resolveOwner = async (principal: string) => ({
|
|
1763
|
+
principal,
|
|
1764
|
+
namespace: "team",
|
|
1765
|
+
storage,
|
|
1766
|
+
});
|
|
1767
|
+
const cardService = new SupportPassportCardService({ resolveOwner, now });
|
|
1768
|
+
const grantStore = new SupportPassportGrantStore({
|
|
1769
|
+
memoryDir: path.join(root, "grants"),
|
|
1770
|
+
now,
|
|
1771
|
+
});
|
|
1772
|
+
const grantService = new SupportPassportGrantService({
|
|
1773
|
+
grantStore,
|
|
1774
|
+
resolveOwner,
|
|
1775
|
+
resolveNamespace: async () => storage,
|
|
1776
|
+
now,
|
|
1777
|
+
});
|
|
1778
|
+
const draft = await cardService.createManualDraft({
|
|
1779
|
+
principal: "owner:alice",
|
|
1780
|
+
title: "Alice card",
|
|
1781
|
+
statement: "Offer Alice a quiet place.",
|
|
1782
|
+
category: "environment",
|
|
1783
|
+
reviewBy: "2026-09-01T12:00:00.000Z",
|
|
1784
|
+
});
|
|
1785
|
+
const aliceCard = await cardService.approveCard({
|
|
1786
|
+
principal: "owner:alice",
|
|
1787
|
+
cardId: draft.cardId,
|
|
1788
|
+
expectedRevision: draft.revision,
|
|
1789
|
+
});
|
|
1790
|
+
|
|
1791
|
+
await assert.rejects(
|
|
1792
|
+
grantService.createGrant({
|
|
1793
|
+
principal: "owner:bob",
|
|
1794
|
+
cards: [{ cardId: aliceCard.cardId, revision: aliceCard.revision }],
|
|
1795
|
+
expiresAt: "2026-08-11T13:00:00.000Z",
|
|
1796
|
+
}),
|
|
1797
|
+
(error: unknown) =>
|
|
1798
|
+
error instanceof SupportPassportError &&
|
|
1799
|
+
error.code === "invalid_card_status",
|
|
1800
|
+
);
|
|
1801
|
+
const forged = await grantStore.create({
|
|
1802
|
+
namespace: "team",
|
|
1803
|
+
principal: "owner:bob",
|
|
1804
|
+
cards: [{ cardId: aliceCard.cardId, revision: aliceCard.revision }],
|
|
1805
|
+
expiresAt: "2026-08-11T13:00:00.000Z",
|
|
1806
|
+
});
|
|
1807
|
+
await assert.rejects(
|
|
1808
|
+
grantService.readGrant({
|
|
1809
|
+
grantId: forged.state.grantId,
|
|
1810
|
+
secret: forged.secret,
|
|
1811
|
+
}),
|
|
1812
|
+
(error: unknown) =>
|
|
1813
|
+
error instanceof SupportPassportError && error.code === "grant_stale",
|
|
1814
|
+
);
|
|
1815
|
+
} finally {
|
|
1816
|
+
StorageManager.clearAllStaticCaches();
|
|
1817
|
+
await rm(root, { recursive: true, force: true });
|
|
1818
|
+
}
|
|
1819
|
+
});
|
|
1820
|
+
|
|
1821
|
+
test("helper snapshots ignore colliding card IDs from other owners and reject owned duplicates", async () => {
|
|
1822
|
+
StorageManager.clearAllStaticCaches();
|
|
1823
|
+
const root = await mkdtemp(path.join(tmpdir(), "remnic-support-grant-snapshot-scope-"));
|
|
1824
|
+
try {
|
|
1825
|
+
const storage = new StorageManager(path.join(root, "shared-storage"));
|
|
1826
|
+
await storage.ensureDirectories();
|
|
1827
|
+
const now = () => new Date("2026-08-11T12:00:00.000Z");
|
|
1828
|
+
const resolveOwner = async (principal: string) => ({ principal, namespace: "team", storage });
|
|
1829
|
+
const cardService = new SupportPassportCardService({ resolveOwner, now });
|
|
1830
|
+
const grantStore = new SupportPassportGrantStore({ memoryDir: path.join(root, "grants"), now });
|
|
1831
|
+
const grantService = new SupportPassportGrantService({
|
|
1832
|
+
grantStore,
|
|
1833
|
+
resolveOwner,
|
|
1834
|
+
resolveNamespace: async () => storage,
|
|
1835
|
+
now,
|
|
1836
|
+
});
|
|
1837
|
+
const draft = await cardService.createManualDraft({
|
|
1838
|
+
principal: "owner:alice",
|
|
1839
|
+
title: "Alice card",
|
|
1840
|
+
statement: "Offer Alice a quiet place.",
|
|
1841
|
+
category: "environment",
|
|
1842
|
+
reviewBy: "2026-09-01T12:00:00.000Z",
|
|
1843
|
+
});
|
|
1844
|
+
const card = await cardService.approveCard({
|
|
1845
|
+
principal: "owner:alice",
|
|
1846
|
+
cardId: draft.cardId,
|
|
1847
|
+
expectedRevision: draft.revision,
|
|
1848
|
+
});
|
|
1849
|
+
const created = await grantService.createGrant({
|
|
1850
|
+
principal: "owner:alice",
|
|
1851
|
+
cards: [{ cardId: card.cardId, revision: card.revision }],
|
|
1852
|
+
expiresAt: "2026-08-11T13:00:00.000Z",
|
|
1853
|
+
});
|
|
1854
|
+
const originalMemories = await storage.readAllMemories();
|
|
1855
|
+
const getCorpusScanVersion = storage.getCorpusScanVersion.bind(storage);
|
|
1856
|
+
let testCorpusVersion = 0;
|
|
1857
|
+
storage.getCorpusScanVersion = () =>
|
|
1858
|
+
`${getCorpusScanVersion()}:test-${testCorpusVersion}`;
|
|
1859
|
+
const aliceMemory = originalMemories.find((memory) => memory.frontmatter.id === card.cardId);
|
|
1860
|
+
assert.ok(aliceMemory);
|
|
1861
|
+
const foreignCollision = {
|
|
1862
|
+
...aliceMemory,
|
|
1863
|
+
path: path.join(storage.dir, "preferences", "foreign-collision.md"),
|
|
1864
|
+
frontmatter: {
|
|
1865
|
+
...aliceMemory.frontmatter,
|
|
1866
|
+
structuredAttributes: {
|
|
1867
|
+
...aliceMemory.frontmatter.structuredAttributes,
|
|
1868
|
+
"support-passport-owner": computeSupportPassportOwnerKey("owner:bob"),
|
|
1869
|
+
},
|
|
1870
|
+
},
|
|
1871
|
+
};
|
|
1872
|
+
storage.readAllMemories = async () => [...originalMemories, foreignCollision];
|
|
1873
|
+
|
|
1874
|
+
const guide = await grantService.readGrant({
|
|
1875
|
+
grantId: created.grant.grantId,
|
|
1876
|
+
secret: created.secret,
|
|
1877
|
+
});
|
|
1878
|
+
assert.deepEqual(guide.cards.map((item) => item.cardId), [card.cardId]);
|
|
1879
|
+
|
|
1880
|
+
const ownedDuplicate = {
|
|
1881
|
+
...aliceMemory,
|
|
1882
|
+
path: path.join(storage.dir, "preferences", "owned-duplicate.md"),
|
|
1883
|
+
};
|
|
1884
|
+
storage.readAllMemories = async () => [...originalMemories, foreignCollision, ownedDuplicate];
|
|
1885
|
+
testCorpusVersion += 1;
|
|
1886
|
+
await assert.rejects(
|
|
1887
|
+
grantService.readGrant({ grantId: created.grant.grantId, secret: created.secret }),
|
|
1888
|
+
(error: unknown) => error instanceof SupportPassportError && error.code === "card_data_invalid",
|
|
1889
|
+
);
|
|
1890
|
+
} finally {
|
|
1891
|
+
StorageManager.clearAllStaticCaches();
|
|
1892
|
+
await rm(root, { recursive: true, force: true });
|
|
1893
|
+
}
|
|
1894
|
+
});
|
|
1895
|
+
|
|
1896
|
+
test("grant creation and card withdrawal cannot interleave", async () => {
|
|
1897
|
+
const subject = await makeSubject();
|
|
1898
|
+
try {
|
|
1899
|
+
const card = await createActiveCard(subject);
|
|
1900
|
+
const readAllMemories = subject.aliceStorage.readAllMemories.bind(
|
|
1901
|
+
subject.aliceStorage,
|
|
1902
|
+
);
|
|
1903
|
+
let releaseRead!: () => void;
|
|
1904
|
+
let markReadStarted!: () => void;
|
|
1905
|
+
const readStarted = new Promise<void>((resolve) => {
|
|
1906
|
+
markReadStarted = resolve;
|
|
1907
|
+
});
|
|
1908
|
+
const readReleased = new Promise<void>((resolve) => {
|
|
1909
|
+
releaseRead = resolve;
|
|
1910
|
+
});
|
|
1911
|
+
let pauseRead = true;
|
|
1912
|
+
subject.aliceStorage.readAllMemories = async (...args) => {
|
|
1913
|
+
const memories = await readAllMemories(...args);
|
|
1914
|
+
if (pauseRead) {
|
|
1915
|
+
pauseRead = false;
|
|
1916
|
+
markReadStarted();
|
|
1917
|
+
await readReleased;
|
|
1918
|
+
}
|
|
1919
|
+
return memories;
|
|
1920
|
+
};
|
|
1921
|
+
|
|
1922
|
+
const createPromise = subject.grantService.createGrant({
|
|
1923
|
+
principal: "owner:alice",
|
|
1924
|
+
cards: [{ cardId: card.cardId, revision: card.revision }],
|
|
1925
|
+
expiresAt: expiryAfter(subject, 3_600_000),
|
|
1926
|
+
});
|
|
1927
|
+
await readStarted;
|
|
1928
|
+
let withdrawalSettled = false;
|
|
1929
|
+
const withdrawalPromise = subject.cardService
|
|
1930
|
+
.withdrawCard({
|
|
1931
|
+
principal: "owner:alice",
|
|
1932
|
+
cardId: card.cardId,
|
|
1933
|
+
expectedRevision: card.revision,
|
|
1934
|
+
})
|
|
1935
|
+
.finally(() => {
|
|
1936
|
+
withdrawalSettled = true;
|
|
1937
|
+
});
|
|
1938
|
+
await new Promise<void>((resolve) => setImmediate(resolve));
|
|
1939
|
+
assert.equal(withdrawalSettled, false);
|
|
1940
|
+
|
|
1941
|
+
releaseRead();
|
|
1942
|
+
const created = await createPromise;
|
|
1943
|
+
assert.equal(created.grant.status, "active");
|
|
1944
|
+
await withdrawalPromise;
|
|
1945
|
+
assert.equal(withdrawalSettled, true);
|
|
1946
|
+
} finally {
|
|
1947
|
+
await subject.cleanup();
|
|
1948
|
+
}
|
|
1949
|
+
});
|
|
1950
|
+
|
|
1951
|
+
test("bad secrets return not found, while valid revoked and expired grants reveal safe lifecycle states", async () => {
|
|
1952
|
+
const subject = await makeSubject();
|
|
1953
|
+
try {
|
|
1954
|
+
const card = await createActiveCard(subject);
|
|
1955
|
+
const created = await subject.grantService.createGrant({
|
|
1956
|
+
principal: "owner:alice",
|
|
1957
|
+
cards: [{ cardId: card.cardId, revision: card.revision }],
|
|
1958
|
+
expiresAt: expiryAfter(subject, 300_000),
|
|
1959
|
+
});
|
|
1960
|
+
|
|
1961
|
+
await assert.rejects(
|
|
1962
|
+
subject.grantService.readGrant({
|
|
1963
|
+
grantId: created.grant.grantId,
|
|
1964
|
+
secret: "x".repeat(43),
|
|
1965
|
+
}),
|
|
1966
|
+
(error: unknown) =>
|
|
1967
|
+
error instanceof SupportPassportError &&
|
|
1968
|
+
error.code === "grant_not_found" &&
|
|
1969
|
+
error.status === 404,
|
|
1970
|
+
);
|
|
1971
|
+
|
|
1972
|
+
await assert.rejects(
|
|
1973
|
+
subject.grantStore.revoke({
|
|
1974
|
+
namespace: "alice",
|
|
1975
|
+
principal: "owner:alice",
|
|
1976
|
+
grantId: created.grant.grantId,
|
|
1977
|
+
expectedStateVersion: created.grant.stateVersion + 1,
|
|
1978
|
+
}),
|
|
1979
|
+
(error: unknown) =>
|
|
1980
|
+
error instanceof SupportPassportError &&
|
|
1981
|
+
error.code === "state_conflict" &&
|
|
1982
|
+
error.status === 409,
|
|
1983
|
+
);
|
|
1984
|
+
|
|
1985
|
+
const peerStore = new SupportPassportGrantStore({
|
|
1986
|
+
memoryDir: path.join(subject.root, "shared"),
|
|
1987
|
+
now: subject.now,
|
|
1988
|
+
});
|
|
1989
|
+
const peerRevoked = await peerStore.revoke({
|
|
1990
|
+
namespace: "alice",
|
|
1991
|
+
principal: "owner:alice",
|
|
1992
|
+
grantId: created.grant.grantId,
|
|
1993
|
+
expectedStateVersion: created.grant.stateVersion,
|
|
1994
|
+
});
|
|
1995
|
+
const repeated = await subject.grantService.revokeGrant({
|
|
1996
|
+
principal: "owner:alice",
|
|
1997
|
+
grantId: created.grant.grantId,
|
|
1998
|
+
expectedStateVersion: created.grant.stateVersion,
|
|
1999
|
+
});
|
|
2000
|
+
assert.equal(peerRevoked.stateVersion, 2);
|
|
2001
|
+
assert.equal(repeated.stateVersion, peerRevoked.stateVersion);
|
|
2002
|
+
assert.equal(repeated.revokedAt, peerRevoked.revokedAt);
|
|
2003
|
+
await assert.rejects(
|
|
2004
|
+
subject.grantService.readGrant({
|
|
2005
|
+
grantId: created.grant.grantId,
|
|
2006
|
+
secret: created.secret,
|
|
2007
|
+
}),
|
|
2008
|
+
(error: unknown) =>
|
|
2009
|
+
error instanceof SupportPassportError &&
|
|
2010
|
+
error.code === "grant_gone" &&
|
|
2011
|
+
error.status === 410,
|
|
2012
|
+
);
|
|
2013
|
+
|
|
2014
|
+
const second = await subject.grantService.createGrant({
|
|
2015
|
+
principal: "owner:alice",
|
|
2016
|
+
cards: [{ cardId: card.cardId, revision: card.revision }],
|
|
2017
|
+
expiresAt: expiryAfter(subject, 300_000),
|
|
2018
|
+
});
|
|
2019
|
+
subject.advance(300_000);
|
|
2020
|
+
await assert.rejects(
|
|
2021
|
+
subject.grantService.readGrant({
|
|
2022
|
+
grantId: second.grant.grantId,
|
|
2023
|
+
secret: second.secret,
|
|
2024
|
+
}),
|
|
2025
|
+
(error: unknown) =>
|
|
2026
|
+
error instanceof SupportPassportError &&
|
|
2027
|
+
error.code === "grant_expired" &&
|
|
2028
|
+
error.status === 410,
|
|
2029
|
+
);
|
|
2030
|
+
} finally {
|
|
2031
|
+
await subject.cleanup();
|
|
2032
|
+
}
|
|
2033
|
+
});
|
|
2034
|
+
|
|
2035
|
+
test("a grant that expires during guide assembly does not return a guide", async () => {
|
|
2036
|
+
const subject = await makeSubject();
|
|
2037
|
+
try {
|
|
2038
|
+
const card = await createActiveCard(subject);
|
|
2039
|
+
const created = await subject.grantService.createGrant({
|
|
2040
|
+
principal: "owner:alice",
|
|
2041
|
+
cards: [{ cardId: card.cardId, revision: card.revision }],
|
|
2042
|
+
expiresAt: expiryAfter(subject, 300_000),
|
|
2043
|
+
});
|
|
2044
|
+
const authenticate = subject.grantStore.authenticate.bind(
|
|
2045
|
+
subject.grantStore,
|
|
2046
|
+
);
|
|
2047
|
+
let advanced = false;
|
|
2048
|
+
subject.grantStore.authenticate = async (grantId, secret) => {
|
|
2049
|
+
const state = await authenticate(grantId, secret);
|
|
2050
|
+
if (!advanced) {
|
|
2051
|
+
advanced = true;
|
|
2052
|
+
subject.advance(300_000);
|
|
2053
|
+
}
|
|
2054
|
+
return state;
|
|
2055
|
+
};
|
|
2056
|
+
|
|
2057
|
+
await assert.rejects(
|
|
2058
|
+
subject.grantService.readGrant({
|
|
2059
|
+
grantId: created.grant.grantId,
|
|
2060
|
+
secret: created.secret,
|
|
2061
|
+
}),
|
|
2062
|
+
(error: unknown) =>
|
|
2063
|
+
error instanceof SupportPassportError && error.code === "grant_expired",
|
|
2064
|
+
);
|
|
2065
|
+
} finally {
|
|
2066
|
+
await subject.cleanup();
|
|
2067
|
+
}
|
|
2068
|
+
});
|
|
2069
|
+
|
|
2070
|
+
test("revocation wins while an optimistic helper card read is still pending", async () => {
|
|
2071
|
+
const subject = await makeSubject();
|
|
2072
|
+
const cardReadStarted = Promise.withResolvers<void>();
|
|
2073
|
+
const releaseCardRead = Promise.withResolvers<void>();
|
|
2074
|
+
try {
|
|
2075
|
+
const card = await createActiveCard(subject);
|
|
2076
|
+
const created = await subject.grantService.createGrant({
|
|
2077
|
+
principal: "owner:alice",
|
|
2078
|
+
cards: [{ cardId: card.cardId, revision: card.revision }],
|
|
2079
|
+
expiresAt: expiryAfter(subject, 3_600_000),
|
|
2080
|
+
});
|
|
2081
|
+
const peerStore = new SupportPassportGrantStore({
|
|
2082
|
+
memoryDir: path.join(subject.root, "shared"),
|
|
2083
|
+
now: subject.now,
|
|
2084
|
+
});
|
|
2085
|
+
const authenticate = subject.grantStore.authenticate.bind(
|
|
2086
|
+
subject.grantStore,
|
|
2087
|
+
);
|
|
2088
|
+
let authentications = 0;
|
|
2089
|
+
subject.grantStore.authenticate = async (grantId, secret) => {
|
|
2090
|
+
const state = await authenticate(grantId, secret);
|
|
2091
|
+
authentications += 1;
|
|
2092
|
+
if (authentications === 1) {
|
|
2093
|
+
cardReadStarted.resolve();
|
|
2094
|
+
await releaseCardRead.promise;
|
|
2095
|
+
}
|
|
2096
|
+
return state;
|
|
2097
|
+
};
|
|
2098
|
+
|
|
2099
|
+
const readPromise = subject.grantService.readGrant({
|
|
2100
|
+
grantId: created.grant.grantId,
|
|
2101
|
+
secret: created.secret,
|
|
2102
|
+
});
|
|
2103
|
+
await cardReadStarted.promise;
|
|
2104
|
+
const revokePromise = peerStore.revoke({
|
|
2105
|
+
namespace: "alice",
|
|
2106
|
+
principal: "owner:alice",
|
|
2107
|
+
grantId: created.grant.grantId,
|
|
2108
|
+
expectedStateVersion: created.grant.stateVersion,
|
|
2109
|
+
});
|
|
2110
|
+
await revokePromise;
|
|
2111
|
+
|
|
2112
|
+
releaseCardRead.resolve();
|
|
2113
|
+
await assert.rejects(
|
|
2114
|
+
readPromise,
|
|
2115
|
+
(error: unknown) =>
|
|
2116
|
+
error instanceof SupportPassportError && error.code === "grant_gone",
|
|
2117
|
+
);
|
|
2118
|
+
} finally {
|
|
2119
|
+
releaseCardRead.resolve();
|
|
2120
|
+
await subject.cleanup();
|
|
2121
|
+
}
|
|
2122
|
+
});
|
|
2123
|
+
|
|
2124
|
+
test("helper reads for different grants do not block each other", async () => {
|
|
2125
|
+
const subject = await makeSubject();
|
|
2126
|
+
const releaseFirst = Promise.withResolvers<void>();
|
|
2127
|
+
try {
|
|
2128
|
+
const firstCard = await createActiveCard(subject, "First card");
|
|
2129
|
+
const secondCard = await createActiveCard(subject, "Second card");
|
|
2130
|
+
const first = await subject.grantService.createGrant({
|
|
2131
|
+
principal: "owner:alice",
|
|
2132
|
+
cards: [{ cardId: firstCard.cardId, revision: firstCard.revision }],
|
|
2133
|
+
expiresAt: expiryAfter(subject, 3_600_000),
|
|
2134
|
+
});
|
|
2135
|
+
const second = await subject.grantService.createGrant({
|
|
2136
|
+
principal: "owner:alice",
|
|
2137
|
+
cards: [{ cardId: secondCard.cardId, revision: secondCard.revision }],
|
|
2138
|
+
expiresAt: expiryAfter(subject, 3_600_000),
|
|
2139
|
+
});
|
|
2140
|
+
const firstStarted = Promise.withResolvers<void>();
|
|
2141
|
+
const authenticate = subject.grantStore.authenticate.bind(
|
|
2142
|
+
subject.grantStore,
|
|
2143
|
+
);
|
|
2144
|
+
subject.grantStore.authenticate = async (grantId, secret) => {
|
|
2145
|
+
const state = await authenticate(grantId, secret);
|
|
2146
|
+
if (grantId === first.grant.grantId) {
|
|
2147
|
+
firstStarted.resolve();
|
|
2148
|
+
await releaseFirst.promise;
|
|
2149
|
+
}
|
|
2150
|
+
return state;
|
|
2151
|
+
};
|
|
2152
|
+
const firstRead = subject.grantService.readGrant({
|
|
2153
|
+
grantId: first.grant.grantId,
|
|
2154
|
+
secret: first.secret,
|
|
2155
|
+
});
|
|
2156
|
+
await firstStarted.promise;
|
|
2157
|
+
const secondGuide = await subject.grantService.readGrant({
|
|
2158
|
+
grantId: second.grant.grantId,
|
|
2159
|
+
secret: second.secret,
|
|
2160
|
+
});
|
|
2161
|
+
releaseFirst.resolve();
|
|
2162
|
+
|
|
2163
|
+
const firstGuide = await firstRead;
|
|
2164
|
+
assert.equal(firstGuide.cards[0]?.cardId, firstCard.cardId);
|
|
2165
|
+
assert.equal(secondGuide.cards[0]?.cardId, secondCard.cardId);
|
|
2166
|
+
} finally {
|
|
2167
|
+
releaseFirst.resolve();
|
|
2168
|
+
await subject.cleanup();
|
|
2169
|
+
}
|
|
2170
|
+
});
|
|
2171
|
+
|
|
2172
|
+
test("helper guide snapshots reuse selected cards from the corpus snapshot", async () => {
|
|
2173
|
+
const subject = await makeSubject();
|
|
2174
|
+
try {
|
|
2175
|
+
const firstCard = await createActiveCard(subject, "First card");
|
|
2176
|
+
const secondCard = await createActiveCard(subject, "Second card");
|
|
2177
|
+
const created = await subject.grantService.createGrant({
|
|
2178
|
+
principal: "owner:alice",
|
|
2179
|
+
cards: [
|
|
2180
|
+
{ cardId: firstCard.cardId, revision: firstCard.revision },
|
|
2181
|
+
{ cardId: secondCard.cardId, revision: secondCard.revision },
|
|
2182
|
+
],
|
|
2183
|
+
expiresAt: expiryAfter(subject, 3_600_000),
|
|
2184
|
+
});
|
|
2185
|
+
subject.aliceStorage.getMemoryById = async () => {
|
|
2186
|
+
throw new Error("guide reads must reuse the corpus snapshot");
|
|
2187
|
+
};
|
|
2188
|
+
|
|
2189
|
+
const guide = await subject.grantService.readGrant({
|
|
2190
|
+
grantId: created.grant.grantId,
|
|
2191
|
+
secret: created.secret,
|
|
2192
|
+
});
|
|
2193
|
+
|
|
2194
|
+
assert.deepEqual(
|
|
2195
|
+
guide.cards.map((card) => card.cardId),
|
|
2196
|
+
[firstCard.cardId, secondCard.cardId],
|
|
2197
|
+
);
|
|
2198
|
+
} finally {
|
|
2199
|
+
await subject.cleanup();
|
|
2200
|
+
}
|
|
2201
|
+
});
|
|
2202
|
+
|
|
2203
|
+
test("final helper guide assembly blocks card withdrawal", async () => {
|
|
2204
|
+
const subject = await makeSubject();
|
|
2205
|
+
const finalAuthenticationStarted = Promise.withResolvers<void>();
|
|
2206
|
+
const releaseFinalAuthentication = Promise.withResolvers<void>();
|
|
2207
|
+
try {
|
|
2208
|
+
const card = await createActiveCard(subject);
|
|
2209
|
+
const created = await subject.grantService.createGrant({
|
|
2210
|
+
principal: "owner:alice",
|
|
2211
|
+
cards: [{ cardId: card.cardId, revision: card.revision }],
|
|
2212
|
+
expiresAt: expiryAfter(subject, 3_600_000),
|
|
2213
|
+
});
|
|
2214
|
+
const authenticate = subject.grantStore.authenticate.bind(
|
|
2215
|
+
subject.grantStore,
|
|
2216
|
+
);
|
|
2217
|
+
let authentications = 0;
|
|
2218
|
+
subject.grantStore.authenticate = async (grantId, secret) => {
|
|
2219
|
+
const state = await authenticate(grantId, secret);
|
|
2220
|
+
authentications += 1;
|
|
2221
|
+
if (authentications === 2) {
|
|
2222
|
+
finalAuthenticationStarted.resolve();
|
|
2223
|
+
await releaseFinalAuthentication.promise;
|
|
2224
|
+
}
|
|
2225
|
+
return state;
|
|
2226
|
+
};
|
|
2227
|
+
|
|
2228
|
+
const readPromise = subject.grantService.readGrant({
|
|
2229
|
+
grantId: created.grant.grantId,
|
|
2230
|
+
secret: created.secret,
|
|
2231
|
+
});
|
|
2232
|
+
await finalAuthenticationStarted.promise;
|
|
2233
|
+
let withdrawalSettled = false;
|
|
2234
|
+
const withdrawalPromise = subject.cardService
|
|
2235
|
+
.withdrawCard({
|
|
2236
|
+
principal: "owner:alice",
|
|
2237
|
+
cardId: card.cardId,
|
|
2238
|
+
expectedRevision: card.revision,
|
|
2239
|
+
})
|
|
2240
|
+
.finally(() => {
|
|
2241
|
+
withdrawalSettled = true;
|
|
2242
|
+
});
|
|
2243
|
+
await new Promise<void>((resolve) => setImmediate(resolve));
|
|
2244
|
+
assert.equal(withdrawalSettled, false);
|
|
2245
|
+
|
|
2246
|
+
releaseFinalAuthentication.resolve();
|
|
2247
|
+
const guide = await readPromise;
|
|
2248
|
+
assert.equal(guide.cards[0]?.cardId, card.cardId);
|
|
2249
|
+
await withdrawalPromise;
|
|
2250
|
+
assert.equal(withdrawalSettled, true);
|
|
2251
|
+
} finally {
|
|
2252
|
+
releaseFinalAuthentication.resolve();
|
|
2253
|
+
await subject.cleanup();
|
|
2254
|
+
}
|
|
2255
|
+
});
|
|
2256
|
+
|
|
2257
|
+
test("final helper guide assembly blocks owner revocation", async () => {
|
|
2258
|
+
const subject = await makeSubject();
|
|
2259
|
+
const finalAuthenticationStarted = Promise.withResolvers<void>();
|
|
2260
|
+
const releaseFinalAuthentication = Promise.withResolvers<void>();
|
|
2261
|
+
try {
|
|
2262
|
+
const card = await createActiveCard(subject);
|
|
2263
|
+
const created = await subject.grantService.createGrant({
|
|
2264
|
+
principal: "owner:alice",
|
|
2265
|
+
cards: [{ cardId: card.cardId, revision: card.revision }],
|
|
2266
|
+
expiresAt: expiryAfter(subject, 3_600_000),
|
|
2267
|
+
});
|
|
2268
|
+
const authenticate = subject.grantStore.authenticate.bind(
|
|
2269
|
+
subject.grantStore,
|
|
2270
|
+
);
|
|
2271
|
+
let authentications = 0;
|
|
2272
|
+
subject.grantStore.authenticate = async (grantId, secret) => {
|
|
2273
|
+
const state = await authenticate(grantId, secret);
|
|
2274
|
+
authentications += 1;
|
|
2275
|
+
if (authentications === 2) {
|
|
2276
|
+
finalAuthenticationStarted.resolve();
|
|
2277
|
+
await releaseFinalAuthentication.promise;
|
|
2278
|
+
}
|
|
2279
|
+
return state;
|
|
2280
|
+
};
|
|
2281
|
+
|
|
2282
|
+
const readPromise = subject.grantService.readGrant({
|
|
2283
|
+
grantId: created.grant.grantId,
|
|
2284
|
+
secret: created.secret,
|
|
2285
|
+
});
|
|
2286
|
+
await finalAuthenticationStarted.promise;
|
|
2287
|
+
let revocationSettled = false;
|
|
2288
|
+
const revocationPromise = subject.grantService
|
|
2289
|
+
.revokeGrant({
|
|
2290
|
+
principal: "owner:alice",
|
|
2291
|
+
grantId: created.grant.grantId,
|
|
2292
|
+
expectedStateVersion: created.grant.stateVersion,
|
|
2293
|
+
})
|
|
2294
|
+
.finally(() => {
|
|
2295
|
+
revocationSettled = true;
|
|
2296
|
+
});
|
|
2297
|
+
await new Promise<void>((resolve) => setImmediate(resolve));
|
|
2298
|
+
assert.equal(revocationSettled, false);
|
|
2299
|
+
|
|
2300
|
+
releaseFinalAuthentication.resolve();
|
|
2301
|
+
const guide = await readPromise;
|
|
2302
|
+
assert.equal(guide.cards[0]?.cardId, card.cardId);
|
|
2303
|
+
await revocationPromise;
|
|
2304
|
+
assert.equal(revocationSettled, true);
|
|
2305
|
+
} finally {
|
|
2306
|
+
releaseFinalAuthentication.resolve();
|
|
2307
|
+
await subject.cleanup();
|
|
2308
|
+
}
|
|
2309
|
+
});
|
|
2310
|
+
|
|
2311
|
+
test("helper guide assembly fails when owner-lock ownership is lost", async () => {
|
|
2312
|
+
const subject = await makeSubject();
|
|
2313
|
+
try {
|
|
2314
|
+
const card = await createActiveCard(subject);
|
|
2315
|
+
const created = await subject.grantService.createGrant({
|
|
2316
|
+
principal: "owner:alice",
|
|
2317
|
+
cards: [{ cardId: card.cardId, revision: card.revision }],
|
|
2318
|
+
expiresAt: expiryAfter(subject, 3_600_000),
|
|
2319
|
+
});
|
|
2320
|
+
const authenticate = subject.grantStore.authenticate.bind(
|
|
2321
|
+
subject.grantStore,
|
|
2322
|
+
);
|
|
2323
|
+
let authentications = 0;
|
|
2324
|
+
subject.grantStore.authenticate = async (grantId, secret) => {
|
|
2325
|
+
const state = await authenticate(grantId, secret);
|
|
2326
|
+
authentications += 1;
|
|
2327
|
+
if (authentications === 2) {
|
|
2328
|
+
const lockPath = supportPassportOwnerLockPath(subject.aliceStorage, {
|
|
2329
|
+
namespace: "alice",
|
|
2330
|
+
principal: "owner:alice",
|
|
2331
|
+
});
|
|
2332
|
+
await writeFile(
|
|
2333
|
+
lockPath,
|
|
2334
|
+
`${process.pid} 00000000-0000-4000-8000-000000000000 peer\n`,
|
|
2335
|
+
);
|
|
2336
|
+
}
|
|
2337
|
+
return state;
|
|
2338
|
+
};
|
|
2339
|
+
|
|
2340
|
+
await assert.rejects(
|
|
2341
|
+
subject.grantService.readGrant({
|
|
2342
|
+
grantId: created.grant.grantId,
|
|
2343
|
+
secret: created.secret,
|
|
2344
|
+
}),
|
|
2345
|
+
(error: unknown) =>
|
|
2346
|
+
error instanceof SupportPassportError &&
|
|
2347
|
+
error.code === "storage_conflict",
|
|
2348
|
+
);
|
|
2349
|
+
} finally {
|
|
2350
|
+
await subject.cleanup();
|
|
2351
|
+
}
|
|
2352
|
+
});
|
|
2353
|
+
|
|
2354
|
+
test("helper guide assembly rechecks the owner lock after grant reauthentication", async () => {
|
|
2355
|
+
const subject = await makeSubject();
|
|
2356
|
+
try {
|
|
2357
|
+
const card = await createActiveCard(subject);
|
|
2358
|
+
const created = await subject.grantService.createGrant({
|
|
2359
|
+
principal: "owner:alice",
|
|
2360
|
+
cards: [{ cardId: card.cardId, revision: card.revision }],
|
|
2361
|
+
expiresAt: expiryAfter(subject, 3_600_000),
|
|
2362
|
+
});
|
|
2363
|
+
const authenticate = subject.grantStore.authenticate.bind(
|
|
2364
|
+
subject.grantStore,
|
|
2365
|
+
);
|
|
2366
|
+
let authentications = 0;
|
|
2367
|
+
subject.grantStore.authenticate = async (grantId, secret) => {
|
|
2368
|
+
const state = await authenticate(grantId, secret);
|
|
2369
|
+
authentications += 1;
|
|
2370
|
+
if (authentications === 3) {
|
|
2371
|
+
const lockPath = supportPassportOwnerLockPath(subject.aliceStorage, {
|
|
2372
|
+
namespace: "alice",
|
|
2373
|
+
principal: "owner:alice",
|
|
2374
|
+
});
|
|
2375
|
+
await writeFile(
|
|
2376
|
+
lockPath,
|
|
2377
|
+
`${process.pid} 00000000-0000-4000-8000-000000000000 peer\n`,
|
|
2378
|
+
);
|
|
2379
|
+
}
|
|
2380
|
+
return state;
|
|
2381
|
+
};
|
|
2382
|
+
|
|
2383
|
+
await assert.rejects(
|
|
2384
|
+
subject.grantService.readGrant({
|
|
2385
|
+
grantId: created.grant.grantId,
|
|
2386
|
+
secret: created.secret,
|
|
2387
|
+
}),
|
|
2388
|
+
(error: unknown) =>
|
|
2389
|
+
error instanceof SupportPassportError &&
|
|
2390
|
+
error.code === "storage_conflict",
|
|
2391
|
+
);
|
|
2392
|
+
assert.equal(authentications, 3);
|
|
2393
|
+
} finally {
|
|
2394
|
+
await subject.cleanup();
|
|
2395
|
+
}
|
|
2396
|
+
});
|
|
2397
|
+
|
|
2398
|
+
test("helper guide assembly refreshes the owner lock after its final snapshot read", async () => {
|
|
2399
|
+
const subject = await makeSubject();
|
|
2400
|
+
try {
|
|
2401
|
+
const card = await createActiveCard(subject);
|
|
2402
|
+
const created = await subject.grantService.createGrant({
|
|
2403
|
+
principal: "owner:alice",
|
|
2404
|
+
cards: [{ cardId: card.cardId, revision: card.revision }],
|
|
2405
|
+
expiresAt: expiryAfter(subject, 3_600_000),
|
|
2406
|
+
});
|
|
2407
|
+
await subject.aliceStorage.writeMemory(
|
|
2408
|
+
"fact",
|
|
2409
|
+
"An unrelated memory invalidates the cached card snapshot.",
|
|
2410
|
+
{ source: "support-passport-test" },
|
|
2411
|
+
);
|
|
2412
|
+
const readAllMemories = subject.aliceStorage.readAllMemories.bind(
|
|
2413
|
+
subject.aliceStorage,
|
|
2414
|
+
);
|
|
2415
|
+
let reads = 0;
|
|
2416
|
+
subject.aliceStorage.readAllMemories = async (...args) => {
|
|
2417
|
+
const memories = await readAllMemories(...args);
|
|
2418
|
+
reads += 1;
|
|
2419
|
+
if (reads === 1) {
|
|
2420
|
+
const lockPath = supportPassportOwnerLockPath(subject.aliceStorage, {
|
|
2421
|
+
namespace: "alice",
|
|
2422
|
+
principal: "owner:alice",
|
|
2423
|
+
});
|
|
2424
|
+
await writeFile(
|
|
2425
|
+
lockPath,
|
|
2426
|
+
`${process.pid} 00000000-0000-4000-8000-000000000000 peer\n`,
|
|
2427
|
+
);
|
|
2428
|
+
}
|
|
2429
|
+
return memories;
|
|
2430
|
+
};
|
|
2431
|
+
|
|
2432
|
+
await assert.rejects(
|
|
2433
|
+
subject.grantService.readGrant({
|
|
2434
|
+
grantId: created.grant.grantId,
|
|
2435
|
+
secret: created.secret,
|
|
2436
|
+
}),
|
|
2437
|
+
(error: unknown) =>
|
|
2438
|
+
error instanceof SupportPassportError &&
|
|
2439
|
+
error.code === "storage_conflict",
|
|
2440
|
+
);
|
|
2441
|
+
assert.equal(reads, 1);
|
|
2442
|
+
} finally {
|
|
2443
|
+
await subject.cleanup();
|
|
2444
|
+
}
|
|
2445
|
+
});
|
|
2446
|
+
|
|
2447
|
+
test("helper guide assembly rechecks expiry after its final owner-lock refresh", async () => {
|
|
2448
|
+
const subject = await makeSubject();
|
|
2449
|
+
try {
|
|
2450
|
+
const card = await createActiveCard(subject);
|
|
2451
|
+
const created = await subject.grantService.createGrant({
|
|
2452
|
+
principal: "owner:alice",
|
|
2453
|
+
cards: [{ cardId: card.cardId, revision: card.revision }],
|
|
2454
|
+
expiresAt: expiryAfter(subject, 300_000),
|
|
2455
|
+
});
|
|
2456
|
+
const withAuthenticatedGrant =
|
|
2457
|
+
subject.grantStore.withAuthenticatedGrant.bind(subject.grantStore);
|
|
2458
|
+
subject.grantStore.withAuthenticatedGrant = async (
|
|
2459
|
+
grantId,
|
|
2460
|
+
secret,
|
|
2461
|
+
task,
|
|
2462
|
+
beforeReturn,
|
|
2463
|
+
) =>
|
|
2464
|
+
await withAuthenticatedGrant(grantId, secret, task, async (state) => {
|
|
2465
|
+
subject.advance(300_000);
|
|
2466
|
+
await beforeReturn?.(state);
|
|
2467
|
+
});
|
|
2468
|
+
|
|
2469
|
+
await assert.rejects(
|
|
2470
|
+
subject.grantService.readGrant({
|
|
2471
|
+
grantId: created.grant.grantId,
|
|
2472
|
+
secret: created.secret,
|
|
2473
|
+
}),
|
|
2474
|
+
(error: unknown) =>
|
|
2475
|
+
error instanceof SupportPassportError && error.code === "grant_expired",
|
|
2476
|
+
);
|
|
2477
|
+
} finally {
|
|
2478
|
+
await subject.cleanup();
|
|
2479
|
+
}
|
|
2480
|
+
});
|
|
2481
|
+
|
|
2482
|
+
test("a changed card makes the whole grant stale without a partial guide", async () => {
|
|
2483
|
+
const subject = await makeSubject();
|
|
2484
|
+
try {
|
|
2485
|
+
const first = await createActiveCard(subject, "First card");
|
|
2486
|
+
const second = await createActiveCard(subject, "Second card");
|
|
2487
|
+
const created = await subject.grantService.createGrant({
|
|
2488
|
+
principal: "owner:alice",
|
|
2489
|
+
cards: [
|
|
2490
|
+
{ cardId: first.cardId, revision: first.revision },
|
|
2491
|
+
{ cardId: second.cardId, revision: second.revision },
|
|
2492
|
+
],
|
|
2493
|
+
expiresAt: expiryAfter(subject, 3_600_000),
|
|
2494
|
+
});
|
|
2495
|
+
await subject.cardService.withdrawCard({
|
|
2496
|
+
principal: "owner:alice",
|
|
2497
|
+
cardId: second.cardId,
|
|
2498
|
+
expectedRevision: second.revision,
|
|
2499
|
+
});
|
|
2500
|
+
|
|
2501
|
+
await assert.rejects(
|
|
2502
|
+
subject.grantService.readGrant({
|
|
2503
|
+
grantId: created.grant.grantId,
|
|
2504
|
+
secret: created.secret,
|
|
2505
|
+
}),
|
|
2506
|
+
(error: unknown) =>
|
|
2507
|
+
error instanceof SupportPassportError &&
|
|
2508
|
+
error.code === "grant_stale" &&
|
|
2509
|
+
error.status === 410,
|
|
2510
|
+
);
|
|
2511
|
+
} finally {
|
|
2512
|
+
await subject.cleanup();
|
|
2513
|
+
}
|
|
2514
|
+
});
|
|
2515
|
+
|
|
2516
|
+
test("an active replacement makes predecessor shares stale before retirement finishes", async () => {
|
|
2517
|
+
const subject = await makeSubject();
|
|
2518
|
+
try {
|
|
2519
|
+
const active = await createActiveCard(subject, "Plan changes");
|
|
2520
|
+
const created = await subject.grantService.createGrant({
|
|
2521
|
+
principal: "owner:alice",
|
|
2522
|
+
cards: [{ cardId: active.cardId, revision: active.revision }],
|
|
2523
|
+
expiresAt: expiryAfter(subject, 3_600_000),
|
|
2524
|
+
});
|
|
2525
|
+
const replacement = await subject.cardService.replaceCard({
|
|
2526
|
+
principal: "owner:alice",
|
|
2527
|
+
cardId: active.cardId,
|
|
2528
|
+
expectedRevision: active.revision,
|
|
2529
|
+
title: "Plan changes",
|
|
2530
|
+
statement: "Tell me early when plans change.",
|
|
2531
|
+
category: "transitions",
|
|
2532
|
+
reviewBy: "2026-09-01T12:00:00.000Z",
|
|
2533
|
+
});
|
|
2534
|
+
const writeFrontmatter =
|
|
2535
|
+
subject.aliceStorage.writeMemoryFrontmatterIfUnchanged.bind(
|
|
2536
|
+
subject.aliceStorage,
|
|
2537
|
+
);
|
|
2538
|
+
subject.aliceStorage.writeMemoryFrontmatterIfUnchanged = async (
|
|
2539
|
+
memory,
|
|
2540
|
+
patch,
|
|
2541
|
+
lifecycle,
|
|
2542
|
+
) => {
|
|
2543
|
+
if (lifecycle?.reasonCode === "support-passport-replacement-pending") {
|
|
2544
|
+
throw new Error("simulated interruption before predecessor retirement");
|
|
2545
|
+
}
|
|
2546
|
+
return await writeFrontmatter(memory, patch, lifecycle);
|
|
2547
|
+
};
|
|
2548
|
+
const approved = await subject.cardService.approveCard({
|
|
2549
|
+
principal: "owner:alice",
|
|
2550
|
+
cardId: replacement.cardId,
|
|
2551
|
+
expectedRevision: replacement.revision,
|
|
2552
|
+
});
|
|
2553
|
+
|
|
2554
|
+
await assert.rejects(
|
|
2555
|
+
subject.grantService.readGrant({
|
|
2556
|
+
grantId: created.grant.grantId,
|
|
2557
|
+
secret: created.secret,
|
|
2558
|
+
}),
|
|
2559
|
+
(error: unknown) =>
|
|
2560
|
+
error instanceof SupportPassportError && error.code === "grant_stale",
|
|
2561
|
+
);
|
|
2562
|
+
await assert.rejects(
|
|
2563
|
+
subject.grantService.createGrant({
|
|
2564
|
+
principal: "owner:alice",
|
|
2565
|
+
cards: [{ cardId: active.cardId, revision: active.revision }],
|
|
2566
|
+
expiresAt: expiryAfter(subject, 3_600_000),
|
|
2567
|
+
}),
|
|
2568
|
+
(error: unknown) =>
|
|
2569
|
+
error instanceof SupportPassportError &&
|
|
2570
|
+
error.code === "invalid_card_status",
|
|
2571
|
+
);
|
|
2572
|
+
const replacementGrant = await subject.grantService.createGrant({
|
|
2573
|
+
principal: "owner:alice",
|
|
2574
|
+
cards: [{ cardId: approved.cardId, revision: approved.revision }],
|
|
2575
|
+
expiresAt: expiryAfter(subject, 3_600_000),
|
|
2576
|
+
});
|
|
2577
|
+
assert.equal(
|
|
2578
|
+
(
|
|
2579
|
+
await subject.grantService.readGrant({
|
|
2580
|
+
grantId: replacementGrant.grant.grantId,
|
|
2581
|
+
secret: replacementGrant.secret,
|
|
2582
|
+
})
|
|
2583
|
+
).cards[0]?.cardId,
|
|
2584
|
+
approved.cardId,
|
|
2585
|
+
);
|
|
2586
|
+
} finally {
|
|
2587
|
+
await subject.cleanup();
|
|
2588
|
+
}
|
|
2589
|
+
});
|
|
2590
|
+
|
|
2591
|
+
test("a peer storage withdrawal invalidates a cached helper guide snapshot", async () => {
|
|
2592
|
+
const subject = await makeSubject();
|
|
2593
|
+
try {
|
|
2594
|
+
const card = await createActiveCard(subject);
|
|
2595
|
+
const created = await subject.grantService.createGrant({
|
|
2596
|
+
principal: "owner:alice",
|
|
2597
|
+
cards: [{ cardId: card.cardId, revision: card.revision }],
|
|
2598
|
+
expiresAt: expiryAfter(subject, 3_600_000),
|
|
2599
|
+
});
|
|
2600
|
+
await subject.grantService.readGrant({ grantId: created.grant.grantId, secret: created.secret });
|
|
2601
|
+
|
|
2602
|
+
const peerStorage = new StorageManager(subject.aliceStorage.dir);
|
|
2603
|
+
await peerStorage.ensureDirectories();
|
|
2604
|
+
const peerCards = new SupportPassportCardService({
|
|
2605
|
+
resolveOwner: async (principal) => ({ principal, namespace: "alice", storage: peerStorage }),
|
|
2606
|
+
now: subject.now,
|
|
2607
|
+
});
|
|
2608
|
+
await peerCards.withdrawCard({
|
|
2609
|
+
principal: "owner:alice",
|
|
2610
|
+
cardId: card.cardId,
|
|
2611
|
+
expectedRevision: card.revision,
|
|
2612
|
+
});
|
|
2613
|
+
|
|
2614
|
+
await assert.rejects(
|
|
2615
|
+
subject.grantService.readGrant({ grantId: created.grant.grantId, secret: created.secret }),
|
|
2616
|
+
(error: unknown) => error instanceof SupportPassportError && error.code === "grant_stale"
|
|
2617
|
+
);
|
|
2618
|
+
} finally {
|
|
2619
|
+
await subject.cleanup();
|
|
2620
|
+
}
|
|
2621
|
+
});
|
|
2622
|
+
|
|
2623
|
+
test("unrelated memory writes do not invalidate an unchanged shared guide", async () => {
|
|
2624
|
+
const subject = await makeSubject();
|
|
2625
|
+
try {
|
|
2626
|
+
const card = await createActiveCard(subject);
|
|
2627
|
+
const created = await subject.grantService.createGrant({
|
|
2628
|
+
principal: "owner:alice",
|
|
2629
|
+
cards: [{ cardId: card.cardId, revision: card.revision }],
|
|
2630
|
+
expiresAt: expiryAfter(subject, 3_600_000),
|
|
2631
|
+
});
|
|
2632
|
+
const readAllMemories = subject.aliceStorage.readAllMemories.bind(
|
|
2633
|
+
subject.aliceStorage,
|
|
2634
|
+
);
|
|
2635
|
+
let wroteUnrelated = false;
|
|
2636
|
+
subject.aliceStorage.readAllMemories = async (...args) => {
|
|
2637
|
+
const memories = await readAllMemories(...args);
|
|
2638
|
+
if (!wroteUnrelated) {
|
|
2639
|
+
wroteUnrelated = true;
|
|
2640
|
+
await subject.aliceStorage.writeMemory(
|
|
2641
|
+
"fact",
|
|
2642
|
+
"An unrelated memory write.",
|
|
2643
|
+
{
|
|
2644
|
+
source: "support-passport-test",
|
|
2645
|
+
},
|
|
2646
|
+
);
|
|
2647
|
+
}
|
|
2648
|
+
return memories;
|
|
2649
|
+
};
|
|
2650
|
+
|
|
2651
|
+
const guide = await subject.grantService.readGrant({
|
|
2652
|
+
grantId: created.grant.grantId,
|
|
2653
|
+
secret: created.secret,
|
|
2654
|
+
});
|
|
2655
|
+
|
|
2656
|
+
assert.equal(guide.cards[0]?.cardId, card.cardId);
|
|
2657
|
+
} finally {
|
|
2658
|
+
await subject.cleanup();
|
|
2659
|
+
}
|
|
2660
|
+
});
|
|
2661
|
+
|
|
2662
|
+
test("one unchanged guide read reuses one fresh corpus snapshot", async () => {
|
|
2663
|
+
const subject = await makeSubject();
|
|
2664
|
+
try {
|
|
2665
|
+
const card = await createActiveCard(subject);
|
|
2666
|
+
const created = await subject.grantService.createGrant({
|
|
2667
|
+
principal: "owner:alice",
|
|
2668
|
+
cards: [{ cardId: card.cardId, revision: card.revision }],
|
|
2669
|
+
expiresAt: expiryAfter(subject, 3_600_000),
|
|
2670
|
+
});
|
|
2671
|
+
await subject.aliceStorage.writeMemory(
|
|
2672
|
+
"fact",
|
|
2673
|
+
"An unrelated memory invalidates the cached card snapshot.",
|
|
2674
|
+
{ source: "support-passport-test" },
|
|
2675
|
+
);
|
|
2676
|
+
const readAllMemories = subject.aliceStorage.readAllMemories.bind(
|
|
2677
|
+
subject.aliceStorage,
|
|
2678
|
+
);
|
|
2679
|
+
let corpusReads = 0;
|
|
2680
|
+
subject.aliceStorage.readAllMemories = async (...args) => {
|
|
2681
|
+
corpusReads += 1;
|
|
2682
|
+
return await readAllMemories(...args);
|
|
2683
|
+
};
|
|
2684
|
+
subject.aliceStorage.getMemoryById = async () => {
|
|
2685
|
+
throw new Error("guide reads must reuse the corpus snapshot");
|
|
2686
|
+
};
|
|
2687
|
+
|
|
2688
|
+
const guide = await subject.grantService.readGrant({
|
|
2689
|
+
grantId: created.grant.grantId,
|
|
2690
|
+
secret: created.secret,
|
|
2691
|
+
});
|
|
2692
|
+
|
|
2693
|
+
assert.equal(guide.cards[0]?.cardId, card.cardId);
|
|
2694
|
+
assert.equal(corpusReads, 1);
|
|
2695
|
+
} finally {
|
|
2696
|
+
await subject.cleanup();
|
|
2697
|
+
}
|
|
2698
|
+
});
|
|
2699
|
+
|
|
2700
|
+
test("grant creation rejects an approved card with an invalid update timestamp", async () => {
|
|
2701
|
+
const subject = await makeSubject();
|
|
2702
|
+
try {
|
|
2703
|
+
const card = await createActiveCard(subject);
|
|
2704
|
+
const memory = await subject.aliceStorage.getMemoryById(card.cardId);
|
|
2705
|
+
assert.ok(memory);
|
|
2706
|
+
assert.equal(
|
|
2707
|
+
await subject.aliceStorage.writeMemoryFrontmatterIfUnchanged(memory, {
|
|
2708
|
+
updated: "2026-08-11T12:00:00+99:99",
|
|
2709
|
+
}),
|
|
2710
|
+
true,
|
|
2711
|
+
);
|
|
2712
|
+
assert.deepEqual(
|
|
2713
|
+
await subject.cardService.listCards({ principal: "owner:alice" }),
|
|
2714
|
+
[],
|
|
2715
|
+
);
|
|
2716
|
+
|
|
2717
|
+
await assert.rejects(
|
|
2718
|
+
subject.grantService.createGrant({
|
|
2719
|
+
principal: "owner:alice",
|
|
2720
|
+
cards: [{ cardId: card.cardId, revision: card.revision }],
|
|
2721
|
+
expiresAt: expiryAfter(subject, 3_600_000),
|
|
2722
|
+
}),
|
|
2723
|
+
(error: unknown) =>
|
|
2724
|
+
error instanceof SupportPassportError &&
|
|
2725
|
+
error.code === "invalid_card_status",
|
|
2726
|
+
);
|
|
2727
|
+
assert.deepEqual(
|
|
2728
|
+
await subject.grantService.listGrants({ principal: "owner:alice" }),
|
|
2729
|
+
[],
|
|
2730
|
+
);
|
|
2731
|
+
} finally {
|
|
2732
|
+
await subject.cleanup();
|
|
2733
|
+
}
|
|
2734
|
+
});
|
|
2735
|
+
|
|
2736
|
+
test("public guides compare card update timestamps as instants", async () => {
|
|
2737
|
+
const subject = await makeSubject();
|
|
2738
|
+
try {
|
|
2739
|
+
const first = await createActiveCard(subject, "Earlier card");
|
|
2740
|
+
const second = await createActiveCard(subject, "Later card");
|
|
2741
|
+
const firstMemory = await subject.aliceStorage.getMemoryById(first.cardId);
|
|
2742
|
+
const secondMemory = await subject.aliceStorage.getMemoryById(
|
|
2743
|
+
second.cardId,
|
|
2744
|
+
);
|
|
2745
|
+
assert.ok(firstMemory);
|
|
2746
|
+
assert.ok(secondMemory);
|
|
2747
|
+
assert.equal(
|
|
2748
|
+
await subject.aliceStorage.writeMemoryFrontmatterIfUnchanged(
|
|
2749
|
+
firstMemory,
|
|
2750
|
+
{
|
|
2751
|
+
updated: "2026-08-11T12:00:00+05:00",
|
|
2752
|
+
},
|
|
2753
|
+
),
|
|
2754
|
+
true,
|
|
2755
|
+
);
|
|
2756
|
+
assert.equal(
|
|
2757
|
+
await subject.aliceStorage.writeMemoryFrontmatterIfUnchanged(
|
|
2758
|
+
secondMemory,
|
|
2759
|
+
{
|
|
2760
|
+
updated: "2026-08-11T10:00:00Z",
|
|
2761
|
+
},
|
|
2762
|
+
),
|
|
2763
|
+
true,
|
|
2764
|
+
);
|
|
2765
|
+
const cards = await subject.cardService.listCards({
|
|
2766
|
+
principal: "owner:alice",
|
|
2767
|
+
});
|
|
2768
|
+
const created = await subject.grantService.createGrant({
|
|
2769
|
+
principal: "owner:alice",
|
|
2770
|
+
cards: cards.map((card) => ({
|
|
2771
|
+
cardId: card.cardId,
|
|
2772
|
+
revision: card.revision,
|
|
2773
|
+
})),
|
|
2774
|
+
expiresAt: expiryAfter(subject, 3_600_000),
|
|
2775
|
+
});
|
|
2776
|
+
|
|
2777
|
+
const guide = await subject.grantService.readGrant({
|
|
2778
|
+
grantId: created.grant.grantId,
|
|
2779
|
+
secret: created.secret,
|
|
2780
|
+
});
|
|
2781
|
+
|
|
2782
|
+
assert.equal(guide.updatedAt, "2026-08-11T10:00:00Z");
|
|
2783
|
+
} finally {
|
|
2784
|
+
await subject.cleanup();
|
|
2785
|
+
}
|
|
2786
|
+
});
|
|
2787
|
+
|
|
2788
|
+
test("grant creation rejects drafts, duplicate cards, and unsafe durations", async () => {
|
|
2789
|
+
const subject = await makeSubject();
|
|
2790
|
+
try {
|
|
2791
|
+
const draft = await subject.cardService.createManualDraft({
|
|
2792
|
+
principal: "owner:alice",
|
|
2793
|
+
title: "Draft card",
|
|
2794
|
+
statement: "Give me time to answer.",
|
|
2795
|
+
category: "communication",
|
|
2796
|
+
reviewBy: "2026-09-01T12:00:00.000Z",
|
|
2797
|
+
});
|
|
2798
|
+
const active = await createActiveCard(subject);
|
|
2799
|
+
|
|
2800
|
+
for (const input of [
|
|
2801
|
+
{
|
|
2802
|
+
cards: [{ cardId: draft.cardId, revision: draft.revision }],
|
|
2803
|
+
expiresAt: expiryAfter(subject, 3_600_000),
|
|
2804
|
+
},
|
|
2805
|
+
{
|
|
2806
|
+
cards: [
|
|
2807
|
+
{ cardId: active.cardId, revision: active.revision },
|
|
2808
|
+
{ cardId: active.cardId, revision: active.revision },
|
|
2809
|
+
],
|
|
2810
|
+
expiresAt: expiryAfter(subject, 3_600_000),
|
|
2811
|
+
},
|
|
2812
|
+
{
|
|
2813
|
+
cards: [{ cardId: active.cardId, revision: active.revision }],
|
|
2814
|
+
expiresAt: expiryAfter(subject, 299_999),
|
|
2815
|
+
},
|
|
2816
|
+
{
|
|
2817
|
+
cards: [{ cardId: active.cardId, revision: active.revision }],
|
|
2818
|
+
expiresAt: expiryAfter(subject, 604_800_001),
|
|
2819
|
+
},
|
|
2820
|
+
{
|
|
2821
|
+
cards: [{ cardId: active.cardId, revision: active.revision }],
|
|
2822
|
+
expiresAt: "2026-08-11T13:00:00+99:99",
|
|
2823
|
+
},
|
|
2824
|
+
]) {
|
|
2825
|
+
await assert.rejects(
|
|
2826
|
+
subject.grantService.createGrant({
|
|
2827
|
+
principal: "owner:alice",
|
|
2828
|
+
...input,
|
|
2829
|
+
}),
|
|
2830
|
+
(error: unknown) => error instanceof SupportPassportError,
|
|
2831
|
+
);
|
|
2832
|
+
}
|
|
2833
|
+
} finally {
|
|
2834
|
+
await subject.cleanup();
|
|
2835
|
+
}
|
|
2836
|
+
});
|
|
2837
|
+
|
|
2838
|
+
test("the grant store rejects a symlinked grant directory", async () => {
|
|
2839
|
+
const root = await mkdtemp(path.join(tmpdir(), "remnic-support-grant-link-"));
|
|
2840
|
+
try {
|
|
2841
|
+
const memoryDir = path.join(root, "memory");
|
|
2842
|
+
const outside = path.join(root, "outside");
|
|
2843
|
+
await mkdir(path.join(memoryDir, "state", "support-passport"), {
|
|
2844
|
+
recursive: true,
|
|
2845
|
+
});
|
|
2846
|
+
await mkdir(outside);
|
|
2847
|
+
await symlink(
|
|
2848
|
+
outside,
|
|
2849
|
+
path.join(memoryDir, "state", "support-passport", "grants"),
|
|
2850
|
+
);
|
|
2851
|
+
const now = new Date("2026-08-11T12:00:00.000Z");
|
|
2852
|
+
const store = new SupportPassportGrantStore({ memoryDir, now: () => now });
|
|
2853
|
+
|
|
2854
|
+
await assert.rejects(
|
|
2855
|
+
store.create({
|
|
2856
|
+
namespace: "alice",
|
|
2857
|
+
principal: "owner:alice",
|
|
2858
|
+
cards: [{ cardId: "card-1", revision: "a".repeat(64) }],
|
|
2859
|
+
expiresAt: new Date(now.getTime() + 300_000).toISOString(),
|
|
2860
|
+
}),
|
|
2861
|
+
/must remain inside the memory directory/,
|
|
2862
|
+
);
|
|
2863
|
+
} finally {
|
|
2864
|
+
await rm(root, { recursive: true, force: true });
|
|
2865
|
+
}
|
|
2866
|
+
});
|
|
2867
|
+
|
|
2868
|
+
test("private file operations select supported directory access strategies", () => {
|
|
2869
|
+
assert.throws(
|
|
2870
|
+
() =>
|
|
2871
|
+
requirePrivateFileDescriptorRoot(
|
|
2872
|
+
"win32",
|
|
2873
|
+
"private file directory cannot be pinned",
|
|
2874
|
+
),
|
|
2875
|
+
/private file directory cannot be pinned/,
|
|
2876
|
+
);
|
|
2877
|
+
assert.equal(
|
|
2878
|
+
requirePrivateFileDescriptorRoot("linux", "unreachable"),
|
|
2879
|
+
"/proc/self/fd",
|
|
2880
|
+
);
|
|
2881
|
+
assert.equal(
|
|
2882
|
+
requirePrivateFileDescriptorRoot("darwin", "unreachable"),
|
|
2883
|
+
"/dev/fd",
|
|
2884
|
+
);
|
|
2885
|
+
});
|
|
2886
|
+
|
|
2887
|
+
test("private directory creation syncs every verified parent entry", async () => {
|
|
2888
|
+
const root = await mkdtemp(
|
|
2889
|
+
path.join(tmpdir(), "remnic-support-private-directory-sync-"),
|
|
2890
|
+
);
|
|
2891
|
+
try {
|
|
2892
|
+
const target = path.join(root, "state", "support-passport", "grants");
|
|
2893
|
+
let syncs = 0;
|
|
2894
|
+
const syncVerifiedParent = async () => {
|
|
2895
|
+
syncs += 1;
|
|
2896
|
+
};
|
|
2897
|
+
|
|
2898
|
+
await ensurePrivateDirectoryNoFollow(
|
|
2899
|
+
root,
|
|
2900
|
+
target,
|
|
2901
|
+
"private directory creation failed",
|
|
2902
|
+
syncVerifiedParent,
|
|
2903
|
+
);
|
|
2904
|
+
assert.equal(syncs, 3);
|
|
2905
|
+
await ensurePrivateDirectoryNoFollow(
|
|
2906
|
+
root,
|
|
2907
|
+
target,
|
|
2908
|
+
"private directory creation failed",
|
|
2909
|
+
syncVerifiedParent,
|
|
2910
|
+
);
|
|
2911
|
+
assert.equal(syncs, 6);
|
|
2912
|
+
} finally {
|
|
2913
|
+
await rm(root, { recursive: true, force: true });
|
|
2914
|
+
}
|
|
2915
|
+
});
|
|
2916
|
+
|
|
2917
|
+
test("private directory creation retries a failed parent sync", async () => {
|
|
2918
|
+
const root = await mkdtemp(
|
|
2919
|
+
path.join(tmpdir(), "remnic-support-private-directory-sync-retry-"),
|
|
2920
|
+
);
|
|
2921
|
+
try {
|
|
2922
|
+
const target = path.join(root, "state", "support-passport", "grants");
|
|
2923
|
+
await assert.rejects(
|
|
2924
|
+
ensurePrivateDirectoryNoFollow(
|
|
2925
|
+
root,
|
|
2926
|
+
target,
|
|
2927
|
+
"private directory creation failed",
|
|
2928
|
+
async () => {
|
|
2929
|
+
throw Object.assign(new Error("simulated directory sync failure"), {
|
|
2930
|
+
code: "EIO",
|
|
2931
|
+
});
|
|
2932
|
+
},
|
|
2933
|
+
),
|
|
2934
|
+
(error: unknown) => (error as NodeJS.ErrnoException).code === "EIO",
|
|
2935
|
+
);
|
|
2936
|
+
assert.equal((await lstat(path.join(root, "state"))).isDirectory(), true);
|
|
2937
|
+
|
|
2938
|
+
let retrySyncs = 0;
|
|
2939
|
+
await ensurePrivateDirectoryNoFollow(
|
|
2940
|
+
root,
|
|
2941
|
+
target,
|
|
2942
|
+
"private directory creation failed",
|
|
2943
|
+
async () => {
|
|
2944
|
+
retrySyncs += 1;
|
|
2945
|
+
},
|
|
2946
|
+
);
|
|
2947
|
+
assert.equal(retrySyncs, 3);
|
|
2948
|
+
} finally {
|
|
2949
|
+
await rm(root, { recursive: true, force: true });
|
|
2950
|
+
}
|
|
2951
|
+
});
|
|
2952
|
+
|
|
2953
|
+
test("private directory creation retries parent sync when rollback cannot remove the child", async () => {
|
|
2954
|
+
const root = await mkdtemp(
|
|
2955
|
+
path.join(tmpdir(), "remnic-support-private-directory-sync-populated-"),
|
|
2956
|
+
);
|
|
2957
|
+
try {
|
|
2958
|
+
const target = path.join(root, "state");
|
|
2959
|
+
let failed = false;
|
|
2960
|
+
await assert.rejects(
|
|
2961
|
+
ensurePrivateDirectoryNoFollow(
|
|
2962
|
+
root,
|
|
2963
|
+
target,
|
|
2964
|
+
"private directory creation failed",
|
|
2965
|
+
async () => {
|
|
2966
|
+
if (failed) return;
|
|
2967
|
+
failed = true;
|
|
2968
|
+
await writeFile(path.join(target, "peer-entry"), "peer");
|
|
2969
|
+
throw Object.assign(new Error("simulated directory sync failure"), {
|
|
2970
|
+
code: "EIO",
|
|
2971
|
+
});
|
|
2972
|
+
},
|
|
2973
|
+
),
|
|
2974
|
+
(error: unknown) => (error as NodeJS.ErrnoException).code === "EIO",
|
|
2975
|
+
);
|
|
2976
|
+
|
|
2977
|
+
let retrySyncs = 0;
|
|
2978
|
+
await ensurePrivateDirectoryNoFollow(
|
|
2979
|
+
root,
|
|
2980
|
+
target,
|
|
2981
|
+
"private directory creation failed",
|
|
2982
|
+
async () => {
|
|
2983
|
+
retrySyncs += 1;
|
|
2984
|
+
},
|
|
2985
|
+
);
|
|
2986
|
+
assert.equal(retrySyncs, 1);
|
|
2987
|
+
} finally {
|
|
2988
|
+
await rm(root, { recursive: true, force: true });
|
|
2989
|
+
}
|
|
2990
|
+
});
|
|
2991
|
+
|
|
2992
|
+
test("a failed parent sync never unlinks a directory opened by a concurrent setup", async () => {
|
|
2993
|
+
const root = await mkdtemp(path.join(tmpdir(), "remnic-support-private-directory-race-"));
|
|
2994
|
+
const releaseFirstSync = Promise.withResolvers<void>();
|
|
2995
|
+
try {
|
|
2996
|
+
const target = path.join(root, "state");
|
|
2997
|
+
const firstSyncStarted = Promise.withResolvers<void>();
|
|
2998
|
+
const first = ensurePrivateDirectoryNoFollow(root, target, "private directory creation failed", async () => {
|
|
2999
|
+
firstSyncStarted.resolve();
|
|
3000
|
+
await releaseFirstSync.promise;
|
|
3001
|
+
throw Object.assign(new Error("simulated directory sync failure"), { code: "EIO" });
|
|
3002
|
+
});
|
|
3003
|
+
await firstSyncStarted.promise;
|
|
3004
|
+
await ensurePrivateDirectoryNoFollow(root, target, "private directory creation failed", async () => undefined);
|
|
3005
|
+
releaseFirstSync.resolve();
|
|
3006
|
+
|
|
3007
|
+
await assert.rejects(first, (error: unknown) => (error as NodeJS.ErrnoException).code === "EIO");
|
|
3008
|
+
assert.equal((await lstat(target)).isDirectory(), true);
|
|
3009
|
+
} finally {
|
|
3010
|
+
releaseFirstSync.resolve();
|
|
3011
|
+
await rm(root, { recursive: true, force: true });
|
|
3012
|
+
}
|
|
3013
|
+
});
|
|
3014
|
+
|
|
3015
|
+
test("private directory tree creation syncs missing memory-root ancestors", async () => {
|
|
3016
|
+
const root = await mkdtemp(
|
|
3017
|
+
path.join(tmpdir(), "remnic-support-private-root-sync-"),
|
|
3018
|
+
);
|
|
3019
|
+
try {
|
|
3020
|
+
const target = path.join(root, "new-parent", "memory");
|
|
3021
|
+
let syncs = 0;
|
|
3022
|
+
|
|
3023
|
+
await ensurePrivateDirectoryTreeNoFollow(
|
|
3024
|
+
target,
|
|
3025
|
+
"private directory creation failed",
|
|
3026
|
+
async () => {
|
|
3027
|
+
syncs += 1;
|
|
3028
|
+
},
|
|
3029
|
+
);
|
|
3030
|
+
|
|
3031
|
+
const expectedSyncs = path.relative(path.parse(target).root, target).split(path.sep).filter(Boolean).length;
|
|
3032
|
+
assert.equal(syncs, expectedSyncs);
|
|
3033
|
+
assert.equal((await lstat(target)).isDirectory(), true);
|
|
3034
|
+
assert.equal((await lstat(target)).mode & 0o777, 0o700);
|
|
3035
|
+
} finally {
|
|
3036
|
+
await rm(root, { recursive: true, force: true });
|
|
3037
|
+
}
|
|
3038
|
+
});
|
|
3039
|
+
|
|
3040
|
+
test("private directory tree ignores a read-only filesystem-root sync", async () => {
|
|
3041
|
+
const root = await mkdtemp(path.join(tmpdir(), "remnic-support-private-root-readonly-"));
|
|
3042
|
+
try {
|
|
3043
|
+
const target = path.join(root, "memory", "state", "support-passport", "grants");
|
|
3044
|
+
let firstSync = true;
|
|
3045
|
+
await ensurePrivateDirectoryTreeNoFollow(target, "private directory creation failed", async () => {
|
|
3046
|
+
if (!firstSync) return;
|
|
3047
|
+
firstSync = false;
|
|
3048
|
+
throw Object.assign(new Error("simulated read-only filesystem root"), { code: "EROFS" });
|
|
3049
|
+
});
|
|
3050
|
+
assert.equal((await lstat(target)).isDirectory(), true);
|
|
3051
|
+
} finally {
|
|
3052
|
+
await rm(root, { recursive: true, force: true });
|
|
3053
|
+
}
|
|
3054
|
+
});
|
|
3055
|
+
|
|
3056
|
+
test("private directory tree creation rejects a symlink in the ancestor chain", async () => {
|
|
3057
|
+
const root = await mkdtemp(
|
|
3058
|
+
path.join(tmpdir(), "remnic-support-private-root-link-"),
|
|
3059
|
+
);
|
|
3060
|
+
try {
|
|
3061
|
+
const outside = path.join(root, "outside");
|
|
3062
|
+
const linked = path.join(root, "linked");
|
|
3063
|
+
const target = path.join(linked, "new-parent", "memory");
|
|
3064
|
+
await mkdir(outside);
|
|
3065
|
+
await symlink(outside, linked);
|
|
3066
|
+
|
|
3067
|
+
await assert.rejects(
|
|
3068
|
+
ensurePrivateDirectoryTreeNoFollow(
|
|
3069
|
+
target,
|
|
3070
|
+
"private directory creation failed",
|
|
3071
|
+
),
|
|
3072
|
+
/private directory creation failed/,
|
|
3073
|
+
);
|
|
3074
|
+
assert.deepEqual(await readdir(outside), []);
|
|
3075
|
+
} finally {
|
|
3076
|
+
await rm(root, { recursive: true, force: true });
|
|
3077
|
+
}
|
|
3078
|
+
});
|
|
3079
|
+
|
|
3080
|
+
test("the grant store rejects a symlink alias in the configured memory root", async () => {
|
|
3081
|
+
const root = await mkdtemp(path.join(tmpdir(), "remnic-support-private-root-alias-"));
|
|
3082
|
+
try {
|
|
3083
|
+
const actual = path.join(root, "actual");
|
|
3084
|
+
const alias = path.join(root, "alias");
|
|
3085
|
+
await mkdir(actual);
|
|
3086
|
+
await symlink(actual, alias);
|
|
3087
|
+
const now = new Date("2026-08-11T12:00:00.000Z");
|
|
3088
|
+
const store = new SupportPassportGrantStore({
|
|
3089
|
+
memoryDir: path.join(alias, "memory"),
|
|
3090
|
+
now: () => now,
|
|
3091
|
+
});
|
|
3092
|
+
|
|
3093
|
+
await assert.rejects(
|
|
3094
|
+
store.create({
|
|
3095
|
+
namespace: "alice",
|
|
3096
|
+
principal: "owner:alice",
|
|
3097
|
+
cards: [{ cardId: "card-1", revision: "a".repeat(64) }],
|
|
3098
|
+
expiresAt: new Date(now.getTime() + 300_000).toISOString(),
|
|
3099
|
+
}),
|
|
3100
|
+
/memory directory must be a stable directory/
|
|
3101
|
+
);
|
|
3102
|
+
assert.deepEqual(await readdir(actual), []);
|
|
3103
|
+
} finally {
|
|
3104
|
+
await rm(root, { recursive: true, force: true });
|
|
3105
|
+
}
|
|
3106
|
+
});
|
|
3107
|
+
|
|
3108
|
+
test("Windows private-file operations fail before mutation", async () => {
|
|
3109
|
+
const root = await mkdtemp(path.join(tmpdir(), "remnic-support-private-win32-"));
|
|
3110
|
+
try {
|
|
3111
|
+
const directory = path.join(root, "state", "support-passport", "grants");
|
|
3112
|
+
const filePath = path.join(directory, "grant.json");
|
|
3113
|
+
const errorMessage = "private Windows file operation failed";
|
|
3114
|
+
await assert.rejects(
|
|
3115
|
+
ensurePrivateDirectoryNoFollow(root, directory, errorMessage, undefined, true, "win32"),
|
|
3116
|
+
new RegExp(errorMessage)
|
|
3117
|
+
);
|
|
3118
|
+
assert.equal(
|
|
3119
|
+
await lstat(directory).then(
|
|
3120
|
+
() => true,
|
|
3121
|
+
() => false
|
|
3122
|
+
),
|
|
3123
|
+
false
|
|
3124
|
+
);
|
|
3125
|
+
assert.equal(
|
|
3126
|
+
await lstat(filePath).then(
|
|
3127
|
+
() => true,
|
|
3128
|
+
() => false
|
|
3129
|
+
),
|
|
3130
|
+
false
|
|
3131
|
+
);
|
|
3132
|
+
} finally {
|
|
3133
|
+
await rm(root, { recursive: true, force: true });
|
|
3134
|
+
}
|
|
3135
|
+
});
|
|
3136
|
+
|
|
3137
|
+
test("grant cleanup never follows a swapped grant directory", async () => {
|
|
3138
|
+
const root = await mkdtemp(
|
|
3139
|
+
path.join(tmpdir(), "remnic-support-grant-cleanup-swap-"),
|
|
3140
|
+
);
|
|
3141
|
+
try {
|
|
3142
|
+
const grantId = "00000000-0000-4000-8000-000000000001";
|
|
3143
|
+
const now = new Date("2026-08-11T12:00:00.000Z");
|
|
3144
|
+
const store = new SupportPassportGrantStore({
|
|
3145
|
+
memoryDir: root,
|
|
3146
|
+
makeGrantId: () => grantId,
|
|
3147
|
+
now: () => now,
|
|
3148
|
+
});
|
|
3149
|
+
await store.create({
|
|
3150
|
+
namespace: "alice",
|
|
3151
|
+
principal: "owner:alice",
|
|
3152
|
+
cards: [{ cardId: "card-1", revision: "a".repeat(64) }],
|
|
3153
|
+
expiresAt: new Date(now.getTime() + 3_600_000).toISOString(),
|
|
3154
|
+
});
|
|
3155
|
+
const grantsDir = path.join(root, "state", "support-passport", "grants");
|
|
3156
|
+
const parkedDir = path.join(root, "parked-grants");
|
|
3157
|
+
const outsideDir = path.join(root, "outside");
|
|
3158
|
+
const fileName = `${grantId}.json`;
|
|
3159
|
+
await mkdir(outsideDir);
|
|
3160
|
+
await writeFile(path.join(outsideDir, fileName), "outside must remain", {
|
|
3161
|
+
mode: 0o600,
|
|
3162
|
+
});
|
|
3163
|
+
renameSync(grantsDir, parkedDir);
|
|
3164
|
+
symlinkSync(outsideDir, grantsDir, "dir");
|
|
3165
|
+
const inspected = store as unknown as {
|
|
3166
|
+
removeGrantStates(grantIds: string[]): Promise<void>;
|
|
3167
|
+
};
|
|
3168
|
+
|
|
3169
|
+
await assert.rejects(
|
|
3170
|
+
inspected.removeGrantStates([grantId]),
|
|
3171
|
+
/regular files in a stable directory/,
|
|
3172
|
+
);
|
|
3173
|
+
assert.equal(
|
|
3174
|
+
await readFile(path.join(outsideDir, fileName), "utf8"),
|
|
3175
|
+
"outside must remain",
|
|
3176
|
+
);
|
|
3177
|
+
assert.equal((await lstat(path.join(parkedDir, fileName))).isFile(), true);
|
|
3178
|
+
} finally {
|
|
3179
|
+
await rm(root, { recursive: true, force: true });
|
|
3180
|
+
}
|
|
3181
|
+
});
|
|
3182
|
+
|
|
3183
|
+
test("grant writes reject a directory swapped after the initial safety check", async () => {
|
|
3184
|
+
const root = await mkdtemp(path.join(tmpdir(), "remnic-support-grant-swap-"));
|
|
3185
|
+
try {
|
|
3186
|
+
const memoryDir = path.join(root, "memory");
|
|
3187
|
+
const outside = path.join(root, "outside");
|
|
3188
|
+
const grantsDir = path.join(
|
|
3189
|
+
memoryDir,
|
|
3190
|
+
"state",
|
|
3191
|
+
"support-passport",
|
|
3192
|
+
"grants",
|
|
3193
|
+
);
|
|
3194
|
+
const parkedDir = path.join(root, "parked-grants");
|
|
3195
|
+
await mkdir(outside, { recursive: true });
|
|
3196
|
+
const now = new Date("2026-08-11T12:00:00.000Z");
|
|
3197
|
+
const store = new SupportPassportGrantStore({
|
|
3198
|
+
memoryDir,
|
|
3199
|
+
now: () => now,
|
|
3200
|
+
});
|
|
3201
|
+
|
|
3202
|
+
await assert.rejects(
|
|
3203
|
+
store.create(
|
|
3204
|
+
{
|
|
3205
|
+
namespace: "alice",
|
|
3206
|
+
principal: "owner:alice",
|
|
3207
|
+
cards: [{ cardId: "card-1", revision: "a".repeat(64) }],
|
|
3208
|
+
expiresAt: new Date(now.getTime() + 300_000).toISOString(),
|
|
3209
|
+
},
|
|
3210
|
+
{
|
|
3211
|
+
beforeCommit: async () => {
|
|
3212
|
+
renameSync(grantsDir, parkedDir);
|
|
3213
|
+
symlinkSync(outside, grantsDir, "dir");
|
|
3214
|
+
},
|
|
3215
|
+
},
|
|
3216
|
+
),
|
|
3217
|
+
/regular files in a stable directory/,
|
|
3218
|
+
);
|
|
3219
|
+
assert.deepEqual(await readdir(outside), []);
|
|
3220
|
+
} finally {
|
|
3221
|
+
await rm(root, { recursive: true, force: true });
|
|
3222
|
+
}
|
|
3223
|
+
});
|
|
3224
|
+
|
|
3225
|
+
test("grant writes reject an ancestor swapped after the initial safety check", async () => {
|
|
3226
|
+
const root = await mkdtemp(
|
|
3227
|
+
path.join(tmpdir(), "remnic-support-grant-ancestor-swap-"),
|
|
3228
|
+
);
|
|
3229
|
+
try {
|
|
3230
|
+
const memoryDir = path.join(root, "memory");
|
|
3231
|
+
const passportDir = path.join(memoryDir, "state", "support-passport");
|
|
3232
|
+
const parkedDir = path.join(root, "parked-support-passport");
|
|
3233
|
+
const outside = path.join(root, "outside-support-passport");
|
|
3234
|
+
await mkdir(path.join(outside, "grants", "owners"), { recursive: true });
|
|
3235
|
+
const acceptLock = (async (_lockPath, _options, task) =>
|
|
3236
|
+
await task(true, {
|
|
3237
|
+
refresh: async () => true,
|
|
3238
|
+
})) as typeof withHeldFileLock;
|
|
3239
|
+
const now = new Date("2026-08-11T12:00:00.000Z");
|
|
3240
|
+
const store = new SupportPassportGrantStore({
|
|
3241
|
+
memoryDir,
|
|
3242
|
+
now: () => now,
|
|
3243
|
+
withHeldFileLock: acceptLock,
|
|
3244
|
+
});
|
|
3245
|
+
const inspected = store as unknown as { addToOwnerIndex(): Promise<void> };
|
|
3246
|
+
inspected.addToOwnerIndex = async () => undefined;
|
|
3247
|
+
|
|
3248
|
+
await assert.rejects(
|
|
3249
|
+
store.create(
|
|
3250
|
+
{
|
|
3251
|
+
namespace: "alice",
|
|
3252
|
+
principal: "owner:alice",
|
|
3253
|
+
cards: [{ cardId: "card-1", revision: "a".repeat(64) }],
|
|
3254
|
+
expiresAt: new Date(now.getTime() + 300_000).toISOString(),
|
|
3255
|
+
},
|
|
3256
|
+
{
|
|
3257
|
+
beforeCommit: async () => {
|
|
3258
|
+
renameSync(passportDir, parkedDir);
|
|
3259
|
+
symlinkSync(outside, passportDir, "dir");
|
|
3260
|
+
},
|
|
3261
|
+
},
|
|
3262
|
+
),
|
|
3263
|
+
/regular files in a stable directory/,
|
|
3264
|
+
);
|
|
3265
|
+
assert.deepEqual(await readdir(path.join(outside, "grants")), ["owners"]);
|
|
3266
|
+
} finally {
|
|
3267
|
+
await rm(root, { recursive: true, force: true });
|
|
3268
|
+
}
|
|
3269
|
+
});
|
|
3270
|
+
|
|
3271
|
+
test("grant reads reject a memory-root ancestor swapped after a completed operation", async () => {
|
|
3272
|
+
const root = await mkdtemp(path.join(tmpdir(), "remnic-support-grant-late-ancestor-swap-"));
|
|
3273
|
+
try {
|
|
3274
|
+
const configuredParent = path.join(root, "configured");
|
|
3275
|
+
const memoryDir = path.join(configuredParent, "memory");
|
|
3276
|
+
const parkedParent = path.join(root, "parked-configured");
|
|
3277
|
+
const outsideParent = path.join(root, "outside");
|
|
3278
|
+
const now = new Date("2026-08-11T12:00:00.000Z");
|
|
3279
|
+
const store = new SupportPassportGrantStore({ memoryDir, now: () => now });
|
|
3280
|
+
await store.create({
|
|
3281
|
+
namespace: "alice",
|
|
3282
|
+
principal: "owner:alice",
|
|
3283
|
+
cards: [{ cardId: "card-1", revision: "a".repeat(64) }],
|
|
3284
|
+
expiresAt: new Date(now.getTime() + 300_000).toISOString(),
|
|
3285
|
+
});
|
|
3286
|
+
await mkdir(path.join(outsideParent, "memory", "state", "support-passport", "grants", "owners"), {
|
|
3287
|
+
recursive: true,
|
|
3288
|
+
});
|
|
3289
|
+
renameSync(configuredParent, parkedParent);
|
|
3290
|
+
symlinkSync(outsideParent, configuredParent, "dir");
|
|
3291
|
+
|
|
3292
|
+
await assert.rejects(
|
|
3293
|
+
store.listForOwner("alice", "owner:alice"),
|
|
3294
|
+
/support passport owner membership must remain inside the memory directory/
|
|
3295
|
+
);
|
|
3296
|
+
assert.deepEqual(await readdir(path.join(outsideParent, "memory", "state", "support-passport", "grants")), [
|
|
3297
|
+
"owners",
|
|
3298
|
+
]);
|
|
3299
|
+
} finally {
|
|
3300
|
+
await rm(root, { recursive: true, force: true });
|
|
3301
|
+
}
|
|
3302
|
+
});
|
|
3303
|
+
|
|
3304
|
+
test("the grant store fails closed for corrupt and symlinked grant files", async () => {
|
|
3305
|
+
const root = await mkdtemp(
|
|
3306
|
+
path.join(tmpdir(), "remnic-support-grant-state-"),
|
|
3307
|
+
);
|
|
3308
|
+
try {
|
|
3309
|
+
const firstGrantId = "00000000-0000-4000-8000-000000000001";
|
|
3310
|
+
const secondGrantId = "00000000-0000-4000-8000-000000000002";
|
|
3311
|
+
const grantIds = [firstGrantId, secondGrantId];
|
|
3312
|
+
const now = new Date("2026-08-11T12:00:00.000Z");
|
|
3313
|
+
const store = new SupportPassportGrantStore({
|
|
3314
|
+
memoryDir: root,
|
|
3315
|
+
makeGrantId: () => grantIds.shift() ?? secondGrantId,
|
|
3316
|
+
now: () => now,
|
|
3317
|
+
});
|
|
3318
|
+
const input = {
|
|
3319
|
+
namespace: "alice",
|
|
3320
|
+
principal: "owner:alice",
|
|
3321
|
+
cards: [{ cardId: "card-1", revision: "a".repeat(64) }],
|
|
3322
|
+
expiresAt: new Date(now.getTime() + 3_600_000).toISOString(),
|
|
3323
|
+
};
|
|
3324
|
+
const corrupt = await store.create(input);
|
|
3325
|
+
const grantDir = path.join(root, "state", "support-passport", "grants");
|
|
3326
|
+
const corruptPath = path.join(grantDir, `${firstGrantId}.json`);
|
|
3327
|
+
await writeFile(corruptPath, "{", { mode: 0o600 });
|
|
3328
|
+
await assert.rejects(store.authenticate(firstGrantId, corrupt.secret));
|
|
3329
|
+
|
|
3330
|
+
const linked = await store.create(input);
|
|
3331
|
+
const linkedPath = path.join(grantDir, `${secondGrantId}.json`);
|
|
3332
|
+
const outsidePath = path.join(root, "outside.json");
|
|
3333
|
+
await writeFile(outsidePath, await readFile(linkedPath), { mode: 0o600 });
|
|
3334
|
+
await rm(linkedPath);
|
|
3335
|
+
await symlink(outsidePath, linkedPath);
|
|
3336
|
+
await assert.rejects(
|
|
3337
|
+
store.authenticate(secondGrantId, linked.secret),
|
|
3338
|
+
/must be regular files/,
|
|
3339
|
+
);
|
|
3340
|
+
await assert.rejects(store.listForOwner("alice", "owner:alice"));
|
|
3341
|
+
} finally {
|
|
3342
|
+
await rm(root, { recursive: true, force: true });
|
|
3343
|
+
}
|
|
3344
|
+
});
|
|
3345
|
+
|
|
3346
|
+
test("the grant store omits a state file whose grant ID does not match its file name", async () => {
|
|
3347
|
+
const root = await mkdtemp(
|
|
3348
|
+
path.join(tmpdir(), "remnic-support-grant-identity-"),
|
|
3349
|
+
);
|
|
3350
|
+
try {
|
|
3351
|
+
const firstGrantId = "00000000-0000-4000-8000-000000000001";
|
|
3352
|
+
const secondGrantId = "00000000-0000-4000-8000-000000000002";
|
|
3353
|
+
const grantIds = [firstGrantId, secondGrantId];
|
|
3354
|
+
const now = new Date("2026-08-11T12:00:00.000Z");
|
|
3355
|
+
const store = new SupportPassportGrantStore({
|
|
3356
|
+
memoryDir: root,
|
|
3357
|
+
makeGrantId: () => grantIds.shift() ?? secondGrantId,
|
|
3358
|
+
now: () => now,
|
|
3359
|
+
});
|
|
3360
|
+
const input = {
|
|
3361
|
+
namespace: "alice",
|
|
3362
|
+
principal: "owner:alice",
|
|
3363
|
+
cards: [{ cardId: "card-1", revision: "a".repeat(64) }],
|
|
3364
|
+
expiresAt: new Date(now.getTime() + 3_600_000).toISOString(),
|
|
3365
|
+
};
|
|
3366
|
+
const first = await store.create(input);
|
|
3367
|
+
const second = await store.create(input);
|
|
3368
|
+
const grantDir = path.join(root, "state", "support-passport", "grants");
|
|
3369
|
+
await writeFile(
|
|
3370
|
+
path.join(grantDir, `${secondGrantId}.json`),
|
|
3371
|
+
await readFile(path.join(grantDir, `${firstGrantId}.json`)),
|
|
3372
|
+
{ mode: 0o600 },
|
|
3373
|
+
);
|
|
3374
|
+
|
|
3375
|
+
await assert.rejects(store.authenticate(secondGrantId, second.secret), /grant ID must match its file name/);
|
|
3376
|
+
assert.deepEqual(
|
|
3377
|
+
(await store.listForOwner("alice", "owner:alice")).map((state) => state.grantId),
|
|
3378
|
+
[firstGrantId]
|
|
3379
|
+
);
|
|
3380
|
+
assert.equal(first.state.grantId, firstGrantId);
|
|
3381
|
+
} finally {
|
|
3382
|
+
await rm(root, { recursive: true, force: true });
|
|
3383
|
+
}
|
|
3384
|
+
});
|
|
3385
|
+
|
|
3386
|
+
test("the grant store rejects unsafe durations and grant ID collisions", async () => {
|
|
3387
|
+
const root = await mkdtemp(
|
|
3388
|
+
path.join(tmpdir(), "remnic-support-grant-collision-"),
|
|
3389
|
+
);
|
|
3390
|
+
try {
|
|
3391
|
+
const grantId = "00000000-0000-4000-8000-000000000001";
|
|
3392
|
+
const now = new Date("2026-08-11T12:00:00.000Z");
|
|
3393
|
+
const store = new SupportPassportGrantStore({
|
|
3394
|
+
memoryDir: root,
|
|
3395
|
+
makeGrantId: () => grantId,
|
|
3396
|
+
now: () => now,
|
|
3397
|
+
});
|
|
3398
|
+
const input = {
|
|
3399
|
+
namespace: "alice",
|
|
3400
|
+
principal: "owner:alice",
|
|
3401
|
+
cards: [{ cardId: "card-1", revision: "a".repeat(64) }],
|
|
3402
|
+
expiresAt: new Date(now.getTime() + 300_000).toISOString(),
|
|
3403
|
+
};
|
|
3404
|
+
await store.create(input);
|
|
3405
|
+
await assert.rejects(
|
|
3406
|
+
store.create(input),
|
|
3407
|
+
(error: unknown) =>
|
|
3408
|
+
error instanceof SupportPassportError &&
|
|
3409
|
+
error.code === "storage_conflict",
|
|
3410
|
+
);
|
|
3411
|
+
await assert.rejects(
|
|
3412
|
+
store.create({
|
|
3413
|
+
...input,
|
|
3414
|
+
expiresAt: new Date(now.getTime() + 299_999).toISOString(),
|
|
3415
|
+
}),
|
|
3416
|
+
(error: unknown) =>
|
|
3417
|
+
error instanceof SupportPassportError && error.code === "invalid_input",
|
|
3418
|
+
);
|
|
3419
|
+
} finally {
|
|
3420
|
+
await rm(root, { recursive: true, force: true });
|
|
3421
|
+
}
|
|
3422
|
+
});
|
|
3423
|
+
|
|
3424
|
+
test("the grant store canonicalizes UUID letter case", async () => {
|
|
3425
|
+
const root = await mkdtemp(
|
|
3426
|
+
path.join(tmpdir(), "remnic-support-grant-id-case-"),
|
|
3427
|
+
);
|
|
3428
|
+
try {
|
|
3429
|
+
const uppercaseGrantId = "AAAAAAAA-AAAA-4AAA-8AAA-AAAAAAAAAAAA";
|
|
3430
|
+
const lowercaseGrantId = uppercaseGrantId.toLowerCase();
|
|
3431
|
+
const now = new Date("2026-08-11T12:00:00.000Z");
|
|
3432
|
+
const store = new SupportPassportGrantStore({
|
|
3433
|
+
memoryDir: root,
|
|
3434
|
+
makeGrantId: () => uppercaseGrantId,
|
|
3435
|
+
now: () => now,
|
|
3436
|
+
});
|
|
3437
|
+
const created = await store.create({
|
|
3438
|
+
namespace: "alice",
|
|
3439
|
+
principal: "owner:alice",
|
|
3440
|
+
cards: [{ cardId: "card-1", revision: "a".repeat(64) }],
|
|
3441
|
+
expiresAt: new Date(now.getTime() + 300_000).toISOString(),
|
|
3442
|
+
});
|
|
3443
|
+
|
|
3444
|
+
assert.equal(created.state.grantId, lowercaseGrantId);
|
|
3445
|
+
assert.equal(
|
|
3446
|
+
(await store.authenticate(uppercaseGrantId, created.secret)).grantId,
|
|
3447
|
+
lowercaseGrantId,
|
|
3448
|
+
);
|
|
3449
|
+
assert.equal(
|
|
3450
|
+
(
|
|
3451
|
+
await store.revoke({
|
|
3452
|
+
grantId: uppercaseGrantId,
|
|
3453
|
+
namespace: "alice",
|
|
3454
|
+
principal: "owner:alice",
|
|
3455
|
+
})
|
|
3456
|
+
).grantId,
|
|
3457
|
+
lowercaseGrantId,
|
|
3458
|
+
);
|
|
3459
|
+
} finally {
|
|
3460
|
+
await rm(root, { recursive: true, force: true });
|
|
3461
|
+
}
|
|
3462
|
+
});
|
|
3463
|
+
|
|
3464
|
+
test("the grant store rejects invalid calendar dates", async () => {
|
|
3465
|
+
const root = await mkdtemp(path.join(tmpdir(), "remnic-support-grant-date-"));
|
|
3466
|
+
try {
|
|
3467
|
+
const now = new Date("2026-02-28T12:00:00.000Z");
|
|
3468
|
+
const store = new SupportPassportGrantStore({
|
|
3469
|
+
memoryDir: root,
|
|
3470
|
+
now: () => now,
|
|
3471
|
+
});
|
|
3472
|
+
|
|
3473
|
+
await assert.rejects(
|
|
3474
|
+
store.create({
|
|
3475
|
+
namespace: "alice",
|
|
3476
|
+
principal: "owner:alice",
|
|
3477
|
+
cards: [{ cardId: "card-1", revision: "a".repeat(64) }],
|
|
3478
|
+
expiresAt: "2026-02-31T12:00:00.000Z",
|
|
3479
|
+
}),
|
|
3480
|
+
(error: unknown) =>
|
|
3481
|
+
error instanceof SupportPassportError && error.code === "invalid_input",
|
|
3482
|
+
);
|
|
3483
|
+
} finally {
|
|
3484
|
+
await rm(root, { recursive: true, force: true });
|
|
3485
|
+
}
|
|
3486
|
+
});
|
|
3487
|
+
|
|
3488
|
+
test("the grant store expands a leading tilde in its memory directory", () => {
|
|
3489
|
+
const store = new SupportPassportGrantStore({
|
|
3490
|
+
memoryDir: "~/support-passport-path-test",
|
|
3491
|
+
});
|
|
3492
|
+
const memoryDir = (store as unknown as { memoryDir: string }).memoryDir;
|
|
3493
|
+
assert.equal(memoryDir.includes(`${path.sep}~${path.sep}`), false);
|
|
3494
|
+
assert.equal(path.basename(memoryDir), "support-passport-path-test");
|
|
3495
|
+
assert.equal(path.isAbsolute(memoryDir), true);
|
|
3496
|
+
});
|