settld 0.2.3 → 0.2.5

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.
Files changed (118) hide show
  1. package/docs/CONFIG.md +12 -0
  2. package/docs/README.md +3 -0
  3. package/docs/ops/HOSTED_BASELINE_R2.md +4 -2
  4. package/docs/ops/MINIMUM_PRODUCTION_TOPOLOGY.md +19 -7
  5. package/docs/ops/PRODUCTION_DEPLOYMENT_CHECKLIST.md +8 -3
  6. package/package.json +3 -1
  7. package/scripts/ci/run-public-onboarding-gate.mjs +136 -0
  8. package/scripts/setup/login.mjs +111 -17
  9. package/scripts/setup/onboard.mjs +176 -40
  10. package/scripts/setup/onboarding-failure-taxonomy.mjs +96 -0
  11. package/scripts/setup/onboarding-state-machine.mjs +102 -0
  12. package/services/magic-link/README.md +343 -0
  13. package/services/magic-link/assets/samples/closepack/known-bad/acceptance/acceptance_criteria.json +1 -0
  14. package/services/magic-link/assets/samples/closepack/known-bad/acceptance/acceptance_evaluation.json +1 -0
  15. package/services/magic-link/assets/samples/closepack/known-bad/attestation/bundle_head_attestation.json +1 -0
  16. package/services/magic-link/assets/samples/closepack/known-bad/evidence/evidence_index.json +1 -0
  17. package/services/magic-link/assets/samples/closepack/known-bad/governance/policy.json +1 -0
  18. package/services/magic-link/assets/samples/closepack/known-bad/governance/revocations.json +1 -0
  19. package/services/magic-link/assets/samples/closepack/known-bad/manifest.json +1 -0
  20. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/attestation/bundle_head_attestation.json +1 -0
  21. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/governance/policy.json +1 -0
  22. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/governance/revocations.json +1 -0
  23. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/invoice/invoice_claim.json +1 -0
  24. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/manifest.json +1 -0
  25. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/metering/metering_report.json +1 -0
  26. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/attestation/bundle_head_attestation.json +1 -0
  27. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/events/events.jsonl +1 -0
  28. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/events/payload_material.jsonl +1 -0
  29. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/governance/global/events/events.jsonl +1 -0
  30. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/governance/global/events/payload_material.jsonl +1 -0
  31. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/governance/global/snapshot.json +1 -0
  32. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/governance/policy.json +1 -0
  33. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/governance/revocations.json +1 -0
  34. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/governance/tenant/events/events.jsonl +0 -0
  35. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/governance/tenant/events/payload_material.jsonl +0 -0
  36. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/governance/tenant/snapshot.json +1 -0
  37. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/job/snapshot.json +1 -0
  38. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/keys/public_keys.json +1 -0
  39. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/manifest.json +1 -0
  40. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/verify/report.json +1 -0
  41. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/verify/verification_report.json +1 -0
  42. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/pricing/pricing_matrix.json +1 -0
  43. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/pricing/pricing_matrix_signatures.json +1 -0
  44. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/settld.json +1 -0
  45. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/verify/verification_report.json +1 -0
  46. package/services/magic-link/assets/samples/closepack/known-bad/settld.json +1 -0
  47. package/services/magic-link/assets/samples/closepack/known-bad/sla/sla_definition.json +1 -0
  48. package/services/magic-link/assets/samples/closepack/known-bad/sla/sla_evaluation.json +1 -0
  49. package/services/magic-link/assets/samples/closepack/known-bad/verify/verification_report.json +1 -0
  50. package/services/magic-link/assets/samples/closepack/known-good/acceptance/acceptance_criteria.json +1 -0
  51. package/services/magic-link/assets/samples/closepack/known-good/acceptance/acceptance_evaluation.json +1 -0
  52. package/services/magic-link/assets/samples/closepack/known-good/attestation/bundle_head_attestation.json +1 -0
  53. package/services/magic-link/assets/samples/closepack/known-good/evidence/evidence_index.json +1 -0
  54. package/services/magic-link/assets/samples/closepack/known-good/governance/policy.json +1 -0
  55. package/services/magic-link/assets/samples/closepack/known-good/governance/revocations.json +1 -0
  56. package/services/magic-link/assets/samples/closepack/known-good/manifest.json +1 -0
  57. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/attestation/bundle_head_attestation.json +1 -0
  58. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/governance/policy.json +1 -0
  59. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/governance/revocations.json +1 -0
  60. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/invoice/invoice_claim.json +1 -0
  61. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/manifest.json +1 -0
  62. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/metering/metering_report.json +1 -0
  63. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/attestation/bundle_head_attestation.json +1 -0
  64. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/events/events.jsonl +1 -0
  65. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/events/payload_material.jsonl +1 -0
  66. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/governance/global/events/events.jsonl +1 -0
  67. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/governance/global/events/payload_material.jsonl +1 -0
  68. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/governance/global/snapshot.json +1 -0
  69. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/governance/policy.json +1 -0
  70. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/governance/revocations.json +1 -0
  71. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/governance/tenant/events/events.jsonl +0 -0
  72. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/governance/tenant/events/payload_material.jsonl +0 -0
  73. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/governance/tenant/snapshot.json +1 -0
  74. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/job/snapshot.json +1 -0
  75. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/keys/public_keys.json +1 -0
  76. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/manifest.json +1 -0
  77. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/verify/report.json +1 -0
  78. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/verify/verification_report.json +1 -0
  79. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/pricing/pricing_matrix.json +1 -0
  80. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/pricing/pricing_matrix_signatures.json +1 -0
  81. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/settld.json +1 -0
  82. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/verify/verification_report.json +1 -0
  83. package/services/magic-link/assets/samples/closepack/known-good/settld.json +1 -0
  84. package/services/magic-link/assets/samples/closepack/known-good/sla/sla_definition.json +1 -0
  85. package/services/magic-link/assets/samples/closepack/known-good/sla/sla_evaluation.json +1 -0
  86. package/services/magic-link/assets/samples/closepack/known-good/verify/verification_report.json +1 -0
  87. package/services/magic-link/assets/samples/trust.json +11 -0
  88. package/services/magic-link/src/audit-log.js +24 -0
  89. package/services/magic-link/src/buyer-auth.js +220 -0
  90. package/services/magic-link/src/buyer-notifications.js +402 -0
  91. package/services/magic-link/src/buyer-users.js +129 -0
  92. package/services/magic-link/src/decision-otp.js +156 -0
  93. package/services/magic-link/src/decisions.js +92 -0
  94. package/services/magic-link/src/ingest-keys.js +137 -0
  95. package/services/magic-link/src/maintenance.js +70 -0
  96. package/services/magic-link/src/onboarding-email-sequence.js +331 -0
  97. package/services/magic-link/src/payment-triggers.js +733 -0
  98. package/services/magic-link/src/pdf.js +149 -0
  99. package/services/magic-link/src/policy.js +69 -0
  100. package/services/magic-link/src/redaction.js +6 -0
  101. package/services/magic-link/src/render-model.js +70 -0
  102. package/services/magic-link/src/retention-gc.js +158 -0
  103. package/services/magic-link/src/run-records.js +496 -0
  104. package/services/magic-link/src/s3.js +171 -0
  105. package/services/magic-link/src/server.js +15788 -0
  106. package/services/magic-link/src/settlement-decisions.js +84 -0
  107. package/services/magic-link/src/smtp.js +202 -0
  108. package/services/magic-link/src/storage-cli.js +88 -0
  109. package/services/magic-link/src/storage-format.js +59 -0
  110. package/services/magic-link/src/tenant-billing.js +115 -0
  111. package/services/magic-link/src/tenant-onboarding.js +467 -0
  112. package/services/magic-link/src/tenant-settings.js +1140 -0
  113. package/services/magic-link/src/usage.js +80 -0
  114. package/services/magic-link/src/verify-queue.js +179 -0
  115. package/services/magic-link/src/verify-worker.js +157 -0
  116. package/services/magic-link/src/webhook-retries.js +542 -0
  117. package/services/magic-link/src/webhooks.js +218 -0
  118. package/src/api/app.js +129 -0
@@ -0,0 +1 @@
1
+ {"generatedAt":"2026-02-02T00:00:00.000Z","listHash":"5c286871ea76001026b765f52296df51f76f4e040b537963fde671850684cf87","listId":"revocations_default_v1","revocations":[],"rotations":[],"schemaVersion":"RevocationList.v1","signature":"IG/Mwb9syG1HtzD7YwEM+eG4dPPhcIqy75SC3GjzoZZcepeWY/YpYVVzxxwBINkaFrr/mtUmq+s+Kizogs75Bw==","signedAt":"2026-02-02T00:00:00.000Z","signerKeyId":"key_4bd1ee0813b265cb6670a1cc"}
@@ -0,0 +1 @@
1
+ {"createdAt":"2026-02-02T00:00:00.000Z","currency":"USD","invoiceId":"invoice_fixture_1","jobProof":{"embeddedPath":"payload/job_proof_bundle","headAttestationHash":"c70b62f9e28ac91fa07073c802495d1801921c0d01924a2538f981a7e18f8d5b","manifestHash":"6775ce8a23cb436fdd6e2809bf79d2b1ac05f76a820094342b0cc68f425ef9d8"},"lineItems":[{"amountCents":"1500","code":"WORK_MINUTES","quantity":"10","unitPriceCents":"150"}],"schemaVersion":"InvoiceClaim.v1","subtotalCents":"1500","tenantId":"tenant_fixture","totalCents":"1500"}
@@ -0,0 +1 @@
1
+ {"createdAt":"2026-02-02T00:00:00.000Z","files":[{"bytes":949,"name":"governance/policy.json","sha256":"ecc617ae2edbddddecbc3580b509b6e2f218787238e883a348a575f278f1f662"},{"bytes":409,"name":"governance/revocations.json","sha256":"7dd89653b90b485f06e17990b064a5cb2d2d1e39e144f019f0ab164432d008b2"},{"bytes":519,"name":"invoice/invoice_claim.json","sha256":"fe64b5bee3e55bfdf61628896a1e92df5b4c9a48973e6ffd594e7448d250537f"},{"bytes":537,"name":"metering/metering_report.json","sha256":"1f19f5309e33a4c133fd60fae5dbe10f50b9a12d9541fc8bb975eff432c72c06"},{"bytes":864,"name":"payload/job_proof_bundle/attestation/bundle_head_attestation.json","sha256":"641947fd28044f926bae840ad343bb4d75d6be44b29e329098210f0af4c28a43"},{"bytes":515,"name":"payload/job_proof_bundle/events/events.jsonl","sha256":"1fbc8ef4a9d8a42efc02e2885b043578f3a0584d4054d98c3f06b1cfa276a126"},{"bytes":186,"name":"payload/job_proof_bundle/events/payload_material.jsonl","sha256":"338953ab5054bdc4a7d83c9ef1fef8d3c83ee78916f32c250636238208de62d3"},{"bytes":778,"name":"payload/job_proof_bundle/governance/global/events/events.jsonl","sha256":"a56f271f30f0c485a0cb80ab65f74b65fd438422d1f9d2e7e1664e5f028ce520"},{"bytes":449,"name":"payload/job_proof_bundle/governance/global/events/payload_material.jsonl","sha256":"1e2b95a0192f324d49a45c4378936d5a32c6548f44648d72ed79522186b0867f"},{"bytes":152,"name":"payload/job_proof_bundle/governance/global/snapshot.json","sha256":"4a5b0d84a0b96dd0fec4650d1f5483b8a4795d0c891595d730bf74a556f4368b"},{"bytes":951,"name":"payload/job_proof_bundle/governance/policy.json","sha256":"e88b6fdf8494a31264f18df85a4f2de96d69be1c03ddbfc901157a03cf02341c"},{"bytes":409,"name":"payload/job_proof_bundle/governance/revocations.json","sha256":"7dd89653b90b485f06e17990b064a5cb2d2d1e39e144f019f0ab164432d008b2"},{"bytes":0,"name":"payload/job_proof_bundle/governance/tenant/events/events.jsonl","sha256":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"},{"bytes":0,"name":"payload/job_proof_bundle/governance/tenant/events/payload_material.jsonl","sha256":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"},{"bytes":66,"name":"payload/job_proof_bundle/governance/tenant/snapshot.json","sha256":"353bc53464b75d5a61a810a29014b9a2fcdcdeb0762d18da745ef15765919400"},{"bytes":138,"name":"payload/job_proof_bundle/job/snapshot.json","sha256":"819c087bd8fc5a6fbf833ff13e31915433bc1687f32e8231bd6263718f31bab8"},{"bytes":470,"name":"payload/job_proof_bundle/keys/public_keys.json","sha256":"e88c404c67d245925a0c7e21d5381428b2ef3df69fd31041a39105c5ec7849eb"},{"bytes":1930,"name":"payload/job_proof_bundle/manifest.json","sha256":"844451c8a0efb43b28c5f4ea44dd291e908590fa00583d7d9cc6934c2143c7fb"},{"bytes":297,"name":"payload/job_proof_bundle/verify/report.json","sha256":"1f7725687929166e2258c0f68f4f50d5419a59a0ef490abd407fbcfc9a4123f8"},{"bytes":1312,"name":"payload/job_proof_bundle/verify/verification_report.json","sha256":"86c22381caf30735783418f603518c8f7301b53cb2e8f4e49326e1e1d46c9f06"},{"bytes":112,"name":"pricing/pricing_matrix.json","sha256":"c43476f394086b2f66ac1df090a303f32713547b64ecf29e83ac6762ddbc1fb3"},{"bytes":346,"name":"pricing/pricing_matrix_signatures.json","sha256":"25c1ebf3c5af800807a1fe1a9b824760099c5171765fb4ef8610cfb91d20564e"},{"bytes":601,"name":"settld.json","sha256":"b6033ab19043026b8c5df1c41ce33446088edebc96a0c1cf564ab3d4484b79af"}],"hashing":{"excludes":["verify/**"],"fileOrder":"path_asc","schemaVersion":"InvoiceBundleManifestHash.v1"},"invoiceId":"invoice_fixture_1","manifestHash":"7b348ab1bb5a84f7899b718a08bd7827bb6b7b213b97ef74142061270fb2233a","protocol":"1.0","schemaVersion":"InvoiceBundleManifest.v1","tenantId":"tenant_fixture","type":"InvoiceBundle.v1"}
@@ -0,0 +1 @@
1
+ {"evidenceRefs":[{"path":"job/snapshot.json","sha256":"819c087bd8fc5a6fbf833ff13e31915433bc1687f32e8231bd6263718f31bab8"}],"generatedAt":"2026-02-02T00:00:00.000Z","invoiceId":"invoice_fixture_1","items":[{"code":"WORK_MINUTES","quantity":"10"}],"jobProof":{"embeddedPath":"payload/job_proof_bundle","headAttestationHash":"c70b62f9e28ac91fa07073c802495d1801921c0d01924a2538f981a7e18f8d5b","manifestHash":"6775ce8a23cb436fdd6e2809bf79d2b1ac05f76a820094342b0cc68f425ef9d8"},"schemaVersion":"MeteringReport.v1","tenantId":"tenant_fixture"}
@@ -0,0 +1 @@
1
+ {"attestationHash":"c70b62f9e28ac91fa07073c802495d1801921c0d01924a2538f981a7e18f8d5b","generatedAt":"2026-02-02T00:00:00.000Z","heads":{"governance":{"global":{"lastChainHash":"92b4cd341b48c58fc8dffea3d6bbc0f62f979965d9271339ced8b8f7bf258e60","lastEventId":"evt_gov_serverA_registered"},"tenant":{"lastChainHash":null,"lastEventId":null}},"job":{"lastChainHash":"e698a6e1d597a4be85d31bdac90a34a66b9d43886ad43f59c0cc4e77efdde78d","lastEventId":"evt_job_created"}},"kind":"JobProofBundle.v1","manifestHash":"6775ce8a23cb436fdd6e2809bf79d2b1ac05f76a820094342b0cc68f425ef9d8","schemaVersion":"BundleHeadAttestation.v1","scope":{"jobId":"job_fixture_1"},"signature":"WqNZduwUjQcSlBF7PnWoRyaBuyeD73eevVB3jz58Fa3xd6J+HT6ivilEdUMaATF4QxA+s2p4sMhJYrHeqKB3CQ==","signedAt":"2026-02-02T00:00:00.000Z","signerKeyId":"key_9ed38005b42d9364601ffab3","tenantId":"tenant_fixture"}
@@ -0,0 +1 @@
1
+ {"actor":{"id":"proxy","type":"system"},"at":"2026-02-02T00:00:00.000Z","chainHash":"e698a6e1d597a4be85d31bdac90a34a66b9d43886ad43f59c0cc4e77efdde78d","id":"evt_job_created","payload":{"jobId":"job_fixture_1"},"payloadHash":"6b46a068dfe3479d2f09f84ae0ed29bb7e2cd9e1ec4c17bccca1c1307a04c47c","prevChainHash":null,"signature":"ZmciBEg0pm549K4dpIE988oH5xwRDeimD9g73v5hHXKeGYa0RXZ3hFkJ6BzxCxODFDlUQzzyJkDkjKSpeOkvDg==","signerKeyId":"key_9ed38005b42d9364601ffab3","streamId":"job_fixture_1","type":"JOB_CREATED","v":1}
@@ -0,0 +1 @@
1
+ {"actor":{"id":"proxy","type":"system"},"at":"2026-02-02T00:00:00.000Z","id":"evt_job_created","payload":{"jobId":"job_fixture_1"},"streamId":"job_fixture_1","type":"JOB_CREATED","v":1}
@@ -0,0 +1 @@
1
+ {"actor":{"id":"proxy","type":"system"},"at":"2026-01-01T00:00:00.000Z","chainHash":"92b4cd341b48c58fc8dffea3d6bbc0f62f979965d9271339ced8b8f7bf258e60","id":"evt_gov_serverA_registered","payload":{"keyId":"key_9ed38005b42d9364601ffab3","publicKeyPem":"-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAQ70D1FtwopWnvJeFsiKdThSP/9QADgSeaZYKip79KQI=\n-----END PUBLIC KEY-----\n","reason":"fixture","registeredAt":"2026-01-01T00:00:00.000Z","tenantId":"tenant_default"},"payloadHash":"d996dc5347a113a6050ed453e7b5feb2f1edb93a5ffb68d4fbca7b015336c61b","prevChainHash":null,"signature":"ovzEbrAqeEhQ7xrz/cIIor85ygkD7bgBvbWxKsQetxKkWjCBDoVUfmPn9UrqF/iNtIZcqBK9TA/SXrNk/VjHAg==","signerKeyId":"key_9ed38005b42d9364601ffab3","streamId":"governance","type":"SERVER_SIGNER_KEY_REGISTERED","v":1}
@@ -0,0 +1 @@
1
+ {"actor":{"id":"proxy","type":"system"},"at":"2026-01-01T00:00:00.000Z","id":"evt_gov_serverA_registered","payload":{"keyId":"key_9ed38005b42d9364601ffab3","publicKeyPem":"-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAQ70D1FtwopWnvJeFsiKdThSP/9QADgSeaZYKip79KQI=\n-----END PUBLIC KEY-----\n","reason":"fixture","registeredAt":"2026-01-01T00:00:00.000Z","tenantId":"tenant_default"},"streamId":"governance","type":"SERVER_SIGNER_KEY_REGISTERED","v":1}
@@ -0,0 +1 @@
1
+ {"lastChainHash":"92b4cd341b48c58fc8dffea3d6bbc0f62f979965d9271339ced8b8f7bf258e60","lastEventId":"evt_gov_serverA_registered","streamId":"governance"}
@@ -0,0 +1 @@
1
+ {"algorithms":["ed25519"],"bundleHeadAttestationSigners":[{"allowedKeyIds":["key_9ed38005b42d9364601ffab3"],"allowedScopes":["global","tenant"],"requireGoverned":true,"requiredPurpose":"server","subjectType":"JobProofBundle.v1"}],"generatedAt":"2026-02-02T00:00:00.000Z","policyHash":"f8588a9eef597dd67d29035ff157b00a79bbf2d17056ce063e9a65a390d005d7","policyId":"governance_policy_default_v2","revocationList":{"path":"governance/revocations.json","sha256":"5c286871ea76001026b765f52296df51f76f4e040b537963fde671850684cf87"},"schemaVersion":"GovernancePolicy.v2","signature":"lxr9K5+X7/oVnIFATriFJW/+i96UUDXPP5ATFseAtqlSe/0juicaoad8U0kMT4Xks6xDHrLLOD/ASEm/DRcVDA==","signedAt":"2026-02-02T00:00:00.000Z","signerKeyId":"key_4bd1ee0813b265cb6670a1cc","verificationReportSigners":[{"allowedKeyIds":["key_9ed38005b42d9364601ffab3"],"allowedScopes":["global","tenant"],"requireGoverned":true,"requiredPurpose":"server","subjectType":"JobProofBundle.v1"}]}
@@ -0,0 +1 @@
1
+ {"generatedAt":"2026-02-02T00:00:00.000Z","listHash":"5c286871ea76001026b765f52296df51f76f4e040b537963fde671850684cf87","listId":"revocations_default_v1","revocations":[],"rotations":[],"schemaVersion":"RevocationList.v1","signature":"IG/Mwb9syG1HtzD7YwEM+eG4dPPhcIqy75SC3GjzoZZcepeWY/YpYVVzxxwBINkaFrr/mtUmq+s+Kizogs75Bw==","signedAt":"2026-02-02T00:00:00.000Z","signerKeyId":"key_4bd1ee0813b265cb6670a1cc"}
@@ -0,0 +1 @@
1
+ {"id":"job_fixture_1","lastChainHash":"e698a6e1d597a4be85d31bdac90a34a66b9d43886ad43f59c0cc4e77efdde78d","lastEventId":"evt_job_created"}
@@ -0,0 +1 @@
1
+ {"generatedAt":"2026-02-02T00:00:00.000Z","keys":[{"createdAt":null,"description":null,"keyId":"key_9ed38005b42d9364601ffab3","publicKeyPem":"-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAQ70D1FtwopWnvJeFsiKdThSP/9QADgSeaZYKip79KQI=\n-----END PUBLIC KEY-----\n","purpose":"server","revokedAt":null,"rotatedAt":null,"status":null,"tenantId":"tenant_fixture","validFrom":null,"validTo":null}],"order":"keyId_asc","schemaVersion":"PublicKeys.v1","tenantId":"tenant_fixture"}
@@ -0,0 +1 @@
1
+ {"files":[{"bytes":515,"name":"events/events.jsonl","sha256":"1fbc8ef4a9d8a42efc02e2885b043578f3a0584d4054d98c3f06b1cfa276a126"},{"bytes":186,"name":"events/payload_material.jsonl","sha256":"338953ab5054bdc4a7d83c9ef1fef8d3c83ee78916f32c250636238208de62d3"},{"bytes":778,"name":"governance/global/events/events.jsonl","sha256":"a56f271f30f0c485a0cb80ab65f74b65fd438422d1f9d2e7e1664e5f028ce520"},{"bytes":449,"name":"governance/global/events/payload_material.jsonl","sha256":"1e2b95a0192f324d49a45c4378936d5a32c6548f44648d72ed79522186b0867f"},{"bytes":152,"name":"governance/global/snapshot.json","sha256":"4a5b0d84a0b96dd0fec4650d1f5483b8a4795d0c891595d730bf74a556f4368b"},{"bytes":951,"name":"governance/policy.json","sha256":"e88b6fdf8494a31264f18df85a4f2de96d69be1c03ddbfc901157a03cf02341c"},{"bytes":409,"name":"governance/revocations.json","sha256":"7dd89653b90b485f06e17990b064a5cb2d2d1e39e144f019f0ab164432d008b2"},{"bytes":0,"name":"governance/tenant/events/events.jsonl","sha256":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"},{"bytes":0,"name":"governance/tenant/events/payload_material.jsonl","sha256":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"},{"bytes":66,"name":"governance/tenant/snapshot.json","sha256":"353bc53464b75d5a61a810a29014b9a2fcdcdeb0762d18da745ef15765919400"},{"bytes":138,"name":"job/snapshot.json","sha256":"819c087bd8fc5a6fbf833ff13e31915433bc1687f32e8231bd6263718f31bab8"},{"bytes":470,"name":"keys/public_keys.json","sha256":"e88c404c67d245925a0c7e21d5381428b2ef3df69fd31041a39105c5ec7849eb"}],"generatedAt":"2026-02-02T00:00:00.000Z","hashing":{"excludes":["verify/**"],"fileOrder":"path_asc","schemaVersion":"ProofBundleManifestHash.v1"},"kind":"JobProofBundle.v1","manifestHash":"6775ce8a23cb436fdd6e2809bf79d2b1ac05f76a820094342b0cc68f425ef9d8","schemaVersion":"ProofBundleManifest.v1","scope":{"jobId":"job_fixture_1"},"tenantId":"tenant_fixture"}
@@ -0,0 +1 @@
1
+ {"artifacts":[],"compiledPolicies":[],"eventChain":{"ok":true},"generatedAt":"2026-02-02T00:00:00.000Z","kind":"JobProofBundle.v1","schemaVersion":"ProofBundleVerifyReport.v1","scope":{"jobId":"job_fixture_1"},"settlementProofRefs":{"checked":0,"errors":[],"ok":true},"tenantId":"tenant_fixture"}
@@ -0,0 +1 @@
1
+ {"bundleHeadAttestation":{"attestationHash":"c70b62f9e28ac91fa07073c802495d1801921c0d01924a2538f981a7e18f8d5b","manifestHash":"6775ce8a23cb436fdd6e2809bf79d2b1ac05f76a820094342b0cc68f425ef9d8","schemaVersion":"BundleHeadAttestation.v1","signedAt":"2026-02-02T00:00:00.000Z","signerKeyId":"key_9ed38005b42d9364601ffab3"},"profile":"strict","reportHash":"7bfcc64af8ec9c5df4d5b87150cab3954d5748deb725be672a0cd464c20b3b29","schemaVersion":"VerificationReport.v1","signature":"4QIO0WgDiudPESt/mGSFC7zXcU7RJnF4MftOQHTB7BvtVenQ7SubgBkl1Kc8jj+GPQdtGbIxWZQRR5pL9OrKBQ==","signedAt":"2026-02-02T00:00:00.000Z","signer":{"governanceEventRef":{"chainHash":"92b4cd341b48c58fc8dffea3d6bbc0f62f979965d9271339ced8b8f7bf258e60","eventId":"evt_gov_serverA_registered","payloadHash":"d996dc5347a113a6050ed453e7b5feb2f1edb93a5ffb68d4fbca7b015336c61b","type":"SERVER_SIGNER_KEY_REGISTERED"},"keyId":"key_9ed38005b42d9364601ffab3","scope":"global"},"signerKeyId":"key_9ed38005b42d9364601ffab3","subject":{"createdAt":"2026-02-02T00:00:00.000Z","manifestHash":"6775ce8a23cb436fdd6e2809bf79d2b1ac05f76a820094342b0cc68f425ef9d8","scope":{"jobId":"job_fixture_1"},"tenantId":"tenant_fixture","type":"JobProofBundle.v1"},"tool":{"commit":"0123456789abcdef0123456789abcdef01234567","name":"settld","version":"0.0.0-fixture"},"warnings":[]}
@@ -0,0 +1 @@
1
+ {"currency":"USD","prices":[{"code":"WORK_MINUTES","unitPriceCents":"150"}],"schemaVersion":"PricingMatrix.v1"}
@@ -0,0 +1 @@
1
+ {"pricingMatrixCanonicalHash":"0fdad09bbc49efc35602932ef73950509de0f4f929bac0af35efeeb4d138c128","schemaVersion":"PricingMatrixSignatures.v2","signatures":[{"signature":"BzQbDsMHYcKavlJznp5nc7++boaRGMzFrfTPst79HrTMp7cIfL5foPq7zYzvOc7QsIqyycFCi/WOzIqha6/KAg==","signedAt":"2026-02-02T00:00:00.000Z","signerKeyId":"key_4bd1ee0813b265cb6670a1cc"}]}
@@ -0,0 +1 @@
1
+ {"createdAt":"2026-02-02T00:00:00.000Z","inputs":{"invoiceClaimHash":"fe64b5bee3e55bfdf61628896a1e92df5b4c9a48973e6ffd594e7448d250537f","jobProofBundleHash":"6775ce8a23cb436fdd6e2809bf79d2b1ac05f76a820094342b0cc68f425ef9d8","jobProofHeadAttestationHash":"c70b62f9e28ac91fa07073c802495d1801921c0d01924a2538f981a7e18f8d5b","meteringReportHash":"1f19f5309e33a4c133fd60fae5dbe10f50b9a12d9541fc8bb975eff432c72c06","pricingMatrixHash":"c43476f394086b2f66ac1df090a303f32713547b64ecf29e83ac6762ddbc1fb3"},"invoiceId":"invoice_fixture_1","protocol":"1.0","tenantId":"tenant_fixture","type":"InvoiceBundle.v1"}
@@ -0,0 +1 @@
1
+ {"bundleHeadAttestation":{"attestationHash":"8b15727b811d865a5187aa9da01066a3271dd6465f6c18e6c8d1b59c0a955755","manifestHash":"7b348ab1bb5a84f7899b718a08bd7827bb6b7b213b97ef74142061270fb2233a","schemaVersion":"BundleHeadAttestation.v1","signedAt":"2026-02-02T00:00:00.000Z","signerKeyId":"key_9ed38005b42d9364601ffab3"},"inputs":{"invoiceClaimHash":"fe64b5bee3e55bfdf61628896a1e92df5b4c9a48973e6ffd594e7448d250537f","jobProofBundleHash":"6775ce8a23cb436fdd6e2809bf79d2b1ac05f76a820094342b0cc68f425ef9d8","jobProofHeadAttestationHash":"c70b62f9e28ac91fa07073c802495d1801921c0d01924a2538f981a7e18f8d5b","meteringReportHash":"1f19f5309e33a4c133fd60fae5dbe10f50b9a12d9541fc8bb975eff432c72c06","pricingMatrixHash":"c43476f394086b2f66ac1df090a303f32713547b64ecf29e83ac6762ddbc1fb3"},"policy":null,"profile":"strict","reportHash":"4ce80dafad18c9a29898f65df7e501d10e81c5705982db859ac2e90d0cf72d97","schemaVersion":"VerificationReport.v1","signature":"7mKxfGQ/dSufj2WY9aWJsuhbAe6PgLkGZdBpU4F5ojU9kyjEXEOLZx2wosmzPGVy6b9isC/kgyqw3kx65kwGDQ==","signedAt":"2026-02-02T00:00:00.000Z","signer":{"governanceEventRef":null,"keyId":"key_9ed38005b42d9364601ffab3","scope":"global"},"signerKeyId":"key_9ed38005b42d9364601ffab3","subject":{"createdAt":"2026-02-02T00:00:00.000Z","invoiceId":"invoice_fixture_1","manifestHash":"7b348ab1bb5a84f7899b718a08bd7827bb6b7b213b97ef74142061270fb2233a","protocol":"1.0","tenantId":"tenant_fixture","type":"InvoiceBundle.v1"},"tool":{"commit":"0123456789abcdef0123456789abcdef01234567","name":"settld","version":"0.0.0-fixture"},"warnings":[]}
@@ -0,0 +1 @@
1
+ {"createdAt":"2026-02-02T00:00:00.000Z","inputs":{"invoiceBundleCreatedAt":"2026-02-02T00:00:00.000Z","invoiceBundleHeadAttestationHash":"8b15727b811d865a5187aa9da01066a3271dd6465f6c18e6c8d1b59c0a955755","invoiceBundleManifestHash":"7b348ab1bb5a84f7899b718a08bd7827bb6b7b213b97ef74142061270fb2233a","invoiceBundleTenantId":"tenant_fixture","invoiceBundleType":"InvoiceBundle.v1","invoiceClaimHash":"fe64b5bee3e55bfdf61628896a1e92df5b4c9a48973e6ffd594e7448d250537f","jobProofBundleHash":"6775ce8a23cb436fdd6e2809bf79d2b1ac05f76a820094342b0cc68f425ef9d8","jobProofHeadAttestationHash":"c70b62f9e28ac91fa07073c802495d1801921c0d01924a2538f981a7e18f8d5b","meteringReportHash":"1f19f5309e33a4c133fd60fae5dbe10f50b9a12d9541fc8bb975eff432c72c06","pricingMatrixHash":"c43476f394086b2f66ac1df090a303f32713547b64ecf29e83ac6762ddbc1fb3"},"invoiceBundle":{"embeddedPath":"payload/invoice_bundle","headAttestationHash":"8b15727b811d865a5187aa9da01066a3271dd6465f6c18e6c8d1b59c0a955755","manifestHash":"7b348ab1bb5a84f7899b718a08bd7827bb6b7b213b97ef74142061270fb2233a"},"invoiceId":"invoice_fixture_1","protocol":"1.0","tenantId":"tenant_fixture","type":"ClosePack.v1"}
@@ -0,0 +1 @@
1
+ {"generatedAt":"2026-02-02T00:00:00.000Z","rules":[],"schemaVersion":"SlaDefinition.v1"}
@@ -0,0 +1 @@
1
+ {"generatedAt":"2026-02-02T00:00:00.000Z","overallStatus":"ok","results":[],"schemaVersion":"SlaEvaluation.v1"}
@@ -0,0 +1 @@
1
+ {"bundleHeadAttestation":{"attestationHash":"eaf7436e7aef6529f7d95031ffc7f741ed68a65d8d2b5e3048f9593076ae6652","manifestHash":"0821f3de3cf3bbe659a61a0c14ca38fc82b22239f039e908a9e4b36e4f27744a","schemaVersion":"BundleHeadAttestation.v1","signedAt":"2026-02-02T00:00:00.000Z","signerKeyId":"key_9ed38005b42d9364601ffab3"},"inputs":{"invoiceBundleCreatedAt":"2026-02-02T00:00:00.000Z","invoiceBundleHeadAttestationHash":"8b15727b811d865a5187aa9da01066a3271dd6465f6c18e6c8d1b59c0a955755","invoiceBundleManifestHash":"7b348ab1bb5a84f7899b718a08bd7827bb6b7b213b97ef74142061270fb2233a","invoiceBundleTenantId":"tenant_fixture","invoiceBundleType":"InvoiceBundle.v1","invoiceClaimHash":"fe64b5bee3e55bfdf61628896a1e92df5b4c9a48973e6ffd594e7448d250537f","jobProofBundleHash":"6775ce8a23cb436fdd6e2809bf79d2b1ac05f76a820094342b0cc68f425ef9d8","jobProofHeadAttestationHash":"c70b62f9e28ac91fa07073c802495d1801921c0d01924a2538f981a7e18f8d5b","meteringReportHash":"1f19f5309e33a4c133fd60fae5dbe10f50b9a12d9541fc8bb975eff432c72c06","pricingMatrixHash":"c43476f394086b2f66ac1df090a303f32713547b64ecf29e83ac6762ddbc1fb3"},"policy":null,"profile":"strict","reportHash":"3f1570dffe2e987105a6fcdabcd6e494006bf4ba0f1ff512b358bb21edac3c6a","schemaVersion":"VerificationReport.v1","signature":"WQPcVQLKsn0ivoKQa41yHDeAjzVrbkUgLCh0KWuQFKNthdT3Dvs7cDWLsp75BatG8Ymu9lq9kjc973mDinFjDA==","signedAt":"2026-02-02T00:00:00.000Z","signer":{"governanceEventRef":null,"keyId":"key_9ed38005b42d9364601ffab3","scope":"global"},"signerKeyId":"key_9ed38005b42d9364601ffab3","subject":{"createdAt":"2026-02-02T00:00:00.000Z","invoiceId":"invoice_fixture_1","manifestHash":"0821f3de3cf3bbe659a61a0c14ca38fc82b22239f039e908a9e4b36e4f27744a","protocol":"1.0","scope":{"invoiceId":"invoice_fixture_1"},"tenantId":"tenant_fixture","type":"ClosePack.v1"},"tool":{"commit":"0123456789abcdef0123456789abcdef01234567","name":"settld","version":"0.0.0-fixture"},"warnings":[]}
@@ -0,0 +1,11 @@
1
+ {
2
+ "governanceRoots": {
3
+ "key_4bd1ee0813b265cb6670a1cc": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAFzw7qxt31VJOSAJCw7zy267twYhbJzf9H0dwm3ZmCDM=\n-----END PUBLIC KEY-----\n"
4
+ },
5
+ "pricingSigners": {
6
+ "key_4bd1ee0813b265cb6670a1cc": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAFzw7qxt31VJOSAJCw7zy267twYhbJzf9H0dwm3ZmCDM=\n-----END PUBLIC KEY-----\n"
7
+ },
8
+ "timeAuthorities": {
9
+ "key_0c6b00150d3433d8d612eef2": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEA9CRBU82m4eS1uHNL6PtKBKPLN0YdtubYFP/f9flGVd0=\n-----END PUBLIC KEY-----\n"
10
+ }
11
+ }
@@ -0,0 +1,24 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ function monthKeyUtc(d) {
5
+ const year = d.getUTCFullYear();
6
+ const month = String(d.getUTCMonth() + 1).padStart(2, "0");
7
+ return `${year}-${month}`;
8
+ }
9
+
10
+ function auditJsonlPath({ dataDir, tenantId, monthKey }) {
11
+ return path.join(dataDir, "audit", tenantId, `${monthKey}.jsonl`);
12
+ }
13
+
14
+ export async function appendAuditRecord({ dataDir, tenantId, record } = {}) {
15
+ const at = typeof record?.at === "string" ? record.at : new Date().toISOString();
16
+ const atMs = Date.parse(at);
17
+ const monthKey = Number.isFinite(atMs) ? monthKeyUtc(new Date(atMs)) : monthKeyUtc(new Date());
18
+
19
+ const fp = auditJsonlPath({ dataDir, tenantId, monthKey });
20
+ await fs.mkdir(path.dirname(fp), { recursive: true });
21
+ await fs.appendFile(fp, JSON.stringify({ schemaVersion: "MagicLinkAuditRecord.v1", ...record, at }) + "\n", "utf8");
22
+ return { ok: true, monthKey };
23
+ }
24
+
@@ -0,0 +1,220 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+
5
+ import { sendSmtpMail } from "./smtp.js";
6
+
7
+ function nowIso() {
8
+ return new Date().toISOString();
9
+ }
10
+
11
+ function sha256Hex(text) {
12
+ return crypto.createHash("sha256").update(String(text ?? ""), "utf8").digest("hex");
13
+ }
14
+
15
+ function clampText(v, { max }) {
16
+ const s = String(v ?? "").trim();
17
+ if (!s) return null;
18
+ return s.length <= max ? s : s.slice(0, Math.max(0, max - 1)) + "…";
19
+ }
20
+
21
+ export function normalizeEmailLower(value) {
22
+ const raw = clampText(value, { max: 320 });
23
+ if (!raw) return null;
24
+ const email = raw.toLowerCase();
25
+ if (!email.includes("@")) return null;
26
+ const [local, domain, ...rest] = email.split("@");
27
+ if (!local || !domain || rest.length) return null;
28
+ if (/\s/.test(email)) return null;
29
+ return email;
30
+ }
31
+
32
+ function otpRecordPath({ dataDir, tenantId, email }) {
33
+ const key = sha256Hex(`${tenantId}\n${email}`);
34
+ return path.join(dataDir, "buyer-otp", tenantId, `${key}.json`);
35
+ }
36
+
37
+ function otpOutboxPath({ dataDir, tenantId, email }) {
38
+ const key = sha256Hex(`${tenantId}\n${email}`);
39
+ return path.join(dataDir, "buyer-otp-outbox", `${tenantId}_${key}.json`);
40
+ }
41
+
42
+ function issueCode6() {
43
+ const n = crypto.randomInt(0, 1_000_000);
44
+ return String(n).padStart(6, "0");
45
+ }
46
+
47
+ export async function issueBuyerOtp({ dataDir, tenantId, email, ttlSeconds, deliveryMode, smtp } = {}) {
48
+ const emailNorm = normalizeEmailLower(email);
49
+ if (!emailNorm) return { ok: false, error: "INVALID_EMAIL", message: "invalid email" };
50
+
51
+ const t = String(tenantId ?? "").trim();
52
+ if (!t) return { ok: false, error: "INVALID_TENANT", message: "tenantId is required" };
53
+
54
+ const ttl = Number.parseInt(String(ttlSeconds ?? ""), 10);
55
+ if (!Number.isInteger(ttl) || ttl <= 0) throw new TypeError("ttlSeconds must be a positive integer");
56
+
57
+ const code = issueCode6();
58
+ const createdAt = nowIso();
59
+ const expiresAt = new Date(Date.now() + ttl * 1000).toISOString();
60
+ const codeSha256 = sha256Hex(`${t}\n${emailNorm}\n${code}`);
61
+
62
+ const record = {
63
+ schemaVersion: "BuyerOtpRecord.v1",
64
+ tenantId: t,
65
+ email: emailNorm,
66
+ codeSha256,
67
+ createdAt,
68
+ expiresAt,
69
+ consumedAt: null,
70
+ attempts: 0
71
+ };
72
+
73
+ const fp = otpRecordPath({ dataDir, tenantId: t, email: emailNorm });
74
+ await fs.mkdir(path.dirname(fp), { recursive: true });
75
+ await fs.writeFile(fp, JSON.stringify(record, null, 2) + "\n", "utf8");
76
+
77
+ const mode = String(deliveryMode ?? "record").trim().toLowerCase();
78
+ if (mode === "record") {
79
+ const outFp = otpOutboxPath({ dataDir, tenantId: t, email: emailNorm });
80
+ await fs.mkdir(path.dirname(outFp), { recursive: true });
81
+ await fs.writeFile(
82
+ outFp,
83
+ JSON.stringify(
84
+ { schemaVersion: "BuyerOtpOutboxRecord.v1", tenantId: t, email: emailNorm, code, createdAt, expiresAt },
85
+ null,
86
+ 2
87
+ ) + "\n",
88
+ "utf8"
89
+ );
90
+ } else if (mode === "log") {
91
+ // eslint-disable-next-line no-console
92
+ console.log(`buyer otp tenant=${t} email=${emailNorm} code=${code} expiresAt=${expiresAt}`);
93
+ } else if (mode === "smtp") {
94
+ const from = typeof smtp?.from === "string" ? smtp.from.trim() : "";
95
+ if (!from) return { ok: false, error: "SMTP_NOT_CONFIGURED", message: "smtp.from is required" };
96
+ try {
97
+ await sendSmtpMail({
98
+ host: smtp?.host,
99
+ port: smtp?.port,
100
+ secure: Boolean(smtp?.secure),
101
+ starttls: smtp?.starttls === undefined ? true : Boolean(smtp?.starttls),
102
+ auth: smtp?.user && smtp?.pass ? { user: smtp.user, pass: smtp.pass } : null,
103
+ from,
104
+ to: emailNorm,
105
+ subject: "Your Settld login code",
106
+ text: `Your login code is: ${code}\n\nThis code expires at: ${expiresAt}\n\nIf you did not request this code, you can ignore this email.\n`
107
+ });
108
+ } catch (err) {
109
+ return { ok: false, error: "SMTP_SEND_FAILED", message: err?.message ?? String(err ?? "smtp failed") };
110
+ }
111
+ } else {
112
+ throw new Error("invalid deliveryMode");
113
+ }
114
+
115
+ return { ok: true, tenantId: t, email: emailNorm, expiresAt };
116
+ }
117
+
118
+ export async function verifyAndConsumeBuyerOtp({ dataDir, tenantId, email, code, maxAttempts }) {
119
+ const emailNorm = normalizeEmailLower(email);
120
+ const codeNorm = clampText(code, { max: 32 });
121
+ const t = String(tenantId ?? "").trim();
122
+ if (!t || !emailNorm || !codeNorm) return { ok: false, error: "OTP_INVALID", message: "tenantId, email, and code are required" };
123
+
124
+ const max = Number.parseInt(String(maxAttempts ?? ""), 10);
125
+ if (!Number.isInteger(max) || max < 1) throw new TypeError("maxAttempts must be an integer >= 1");
126
+
127
+ const fp = otpRecordPath({ dataDir, tenantId: t, email: emailNorm });
128
+ let rec = null;
129
+ try {
130
+ rec = JSON.parse(await fs.readFile(fp, "utf8"));
131
+ } catch {
132
+ rec = null;
133
+ }
134
+ if (!rec || typeof rec !== "object" || Array.isArray(rec) || rec.schemaVersion !== "BuyerOtpRecord.v1") {
135
+ return { ok: false, error: "OTP_MISSING", message: "no active otp" };
136
+ }
137
+ if (String(rec.tenantId ?? "") !== t || String(rec.email ?? "") !== emailNorm) {
138
+ return { ok: false, error: "OTP_MISSING", message: "no active otp" };
139
+ }
140
+ if (typeof rec.consumedAt === "string" && rec.consumedAt) return { ok: false, error: "OTP_CONSUMED", message: "otp already used" };
141
+
142
+ const expiresMs = Date.parse(String(rec.expiresAt ?? ""));
143
+ if (!Number.isFinite(expiresMs) || Date.now() > expiresMs) return { ok: false, error: "OTP_EXPIRED", message: "otp expired" };
144
+
145
+ const attempts = Number.parseInt(String(rec.attempts ?? "0"), 10);
146
+ if (Number.isInteger(attempts) && attempts >= max) return { ok: false, error: "OTP_LOCKED", message: "too many attempts" };
147
+
148
+ const expected = String(rec.codeSha256 ?? "");
149
+ const actual = sha256Hex(`${t}\n${emailNorm}\n${codeNorm}`);
150
+ if (expected !== actual) {
151
+ rec.attempts = (Number.isInteger(attempts) ? attempts : 0) + 1;
152
+ await fs.writeFile(fp, JSON.stringify(rec, null, 2) + "\n", "utf8");
153
+ return { ok: false, error: "OTP_INVALID", message: "invalid code" };
154
+ }
155
+
156
+ rec.consumedAt = nowIso();
157
+ rec.attempts = Number.isInteger(attempts) ? attempts : 0;
158
+ await fs.writeFile(fp, JSON.stringify(rec, null, 2) + "\n", "utf8");
159
+ return { ok: true, tenantId: t, email: emailNorm };
160
+ }
161
+
162
+ function hmacBase64Url(key, text) {
163
+ return crypto.createHmac("sha256", key).update(String(text ?? ""), "utf8").digest("base64url");
164
+ }
165
+
166
+ function safeEqualBase64Url(a, b) {
167
+ try {
168
+ const aa = Buffer.from(String(a ?? ""), "base64url");
169
+ const bb = Buffer.from(String(b ?? ""), "base64url");
170
+ if (aa.length !== bb.length) return false;
171
+ return crypto.timingSafeEqual(aa, bb);
172
+ } catch {
173
+ return false;
174
+ }
175
+ }
176
+
177
+ export function createBuyerSessionToken({ sessionKey, tenantId, email, ttlSeconds }) {
178
+ const t = String(tenantId ?? "").trim();
179
+ const e = normalizeEmailLower(email);
180
+ const ttl = Number.parseInt(String(ttlSeconds ?? ""), 10);
181
+ if (!t || !e) return { ok: false, error: "INVALID_SESSION_INPUT" };
182
+ if (!Buffer.isBuffer(sessionKey) || sessionKey.length < 16) return { ok: false, error: "SESSION_KEY_MISSING" };
183
+ if (!Number.isInteger(ttl) || ttl <= 0) return { ok: false, error: "INVALID_TTL" };
184
+
185
+ const issuedAt = nowIso();
186
+ const expiresAt = new Date(Date.now() + ttl * 1000).toISOString();
187
+ const nonce = crypto.randomBytes(16).toString("hex");
188
+ const payload = { schemaVersion: "MagicLinkBuyerSession.v1", tenantId: t, email: e, issuedAt, expiresAt, nonce };
189
+ const payloadB64 = Buffer.from(JSON.stringify(payload), "utf8").toString("base64url");
190
+ const sigB64 = hmacBase64Url(sessionKey, payloadB64);
191
+ return { ok: true, token: `${payloadB64}.${sigB64}`, payload };
192
+ }
193
+
194
+ export function verifyBuyerSessionToken({ sessionKey, token }) {
195
+ if (!Buffer.isBuffer(sessionKey) || sessionKey.length < 16) return { ok: false, error: "SESSION_KEY_MISSING" };
196
+ const raw = String(token ?? "").trim();
197
+ const parts = raw.split(".");
198
+ if (parts.length !== 2) return { ok: false, error: "SESSION_INVALID" };
199
+ const [payloadB64, sigB64] = parts;
200
+ const expected = hmacBase64Url(sessionKey, payloadB64);
201
+ if (!safeEqualBase64Url(expected, sigB64)) return { ok: false, error: "SESSION_INVALID" };
202
+
203
+ let payload = null;
204
+ try {
205
+ payload = JSON.parse(Buffer.from(payloadB64, "base64url").toString("utf8"));
206
+ } catch {
207
+ payload = null;
208
+ }
209
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) return { ok: false, error: "SESSION_INVALID" };
210
+ if (String(payload.schemaVersion ?? "") !== "MagicLinkBuyerSession.v1") return { ok: false, error: "SESSION_INVALID" };
211
+
212
+ const tenantId = String(payload.tenantId ?? "").trim();
213
+ const email = normalizeEmailLower(payload.email);
214
+ if (!tenantId || !email) return { ok: false, error: "SESSION_INVALID" };
215
+
216
+ const expiresMs = Date.parse(String(payload.expiresAt ?? ""));
217
+ if (!Number.isFinite(expiresMs) || Date.now() > expiresMs) return { ok: false, error: "SESSION_EXPIRED" };
218
+
219
+ return { ok: true, payload: { ...payload, tenantId, email } };
220
+ }