@happyvertical/smrt-approvals 0.0.0-stage → 0.55.10

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 (38) hide show
  1. package/AGENTS.md +45 -0
  2. package/CLAUDE.md +1 -0
  3. package/LICENSE +7 -0
  4. package/README.md +124 -2
  5. package/agents/approvals.md +93 -0
  6. package/dist/__smrt-register__.d.ts +2 -0
  7. package/dist/__smrt-register__.d.ts.map +1 -0
  8. package/dist/collections/ApprovalEventCollection.d.ts +13 -0
  9. package/dist/collections/ApprovalEventCollection.d.ts.map +1 -0
  10. package/dist/collections/ApprovalPolicyCollection.d.ts +13 -0
  11. package/dist/collections/ApprovalPolicyCollection.d.ts.map +1 -0
  12. package/dist/collections/ApprovalRequestCollection.d.ts +13 -0
  13. package/dist/collections/ApprovalRequestCollection.d.ts.map +1 -0
  14. package/dist/index.d.ts +46 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +1307 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/kinds.d.ts +61 -0
  19. package/dist/kinds.d.ts.map +1 -0
  20. package/dist/manifest.json +1531 -0
  21. package/dist/models/ApprovalEvent.d.ts +82 -0
  22. package/dist/models/ApprovalEvent.d.ts.map +1 -0
  23. package/dist/models/ApprovalPolicy.d.ts +47 -0
  24. package/dist/models/ApprovalPolicy.d.ts.map +1 -0
  25. package/dist/models/ApprovalRequest.d.ts +118 -0
  26. package/dist/models/ApprovalRequest.d.ts.map +1 -0
  27. package/dist/permissions.d.ts +20 -0
  28. package/dist/permissions.d.ts.map +1 -0
  29. package/dist/service.d.ts +203 -0
  30. package/dist/service.d.ts.map +1 -0
  31. package/dist/smrt-knowledge.json +751 -0
  32. package/dist/types.d.ts +136 -0
  33. package/dist/types.d.ts.map +1 -0
  34. package/dist/types.js +45 -0
  35. package/dist/types.js.map +1 -0
  36. package/dist/write-capability.d.ts +28 -0
  37. package/dist/write-capability.d.ts.map +1 -0
  38. package/package.json +64 -3
package/dist/index.js ADDED
@@ -0,0 +1,1307 @@
1
+ import { APPROVAL_EVENT_TYPES, APPROVAL_PRINCIPAL_TYPES, APPROVAL_STATUSES, ApprovalError, approvalPrincipalFromPermissions } from "./types.js";
2
+ import { ObjectRegistry, SmrtCollection, SmrtObject, appendChange, field, foreignKey, isEmbeddedDatabase, isUniqueViolationError, smrt, withEmbeddedWriteTransaction } from "@happyvertical/smrt-core";
3
+ import { registerPermissionDefinitions } from "@happyvertical/smrt-users";
4
+ import { TenantScoped, getTenantId, isSystemContext, tenantId, withTenant } from "@happyvertical/smrt-tenancy";
5
+ //#region src/__smrt-register__.ts
6
+ ObjectRegistry.registerPackageManifest(JSON.parse(`{"version":"1.0.0","timestamp":0,"packageName":"@happyvertical/smrt-approvals","packageVersion":"0.55.10","objects":{"@happyvertical/smrt-approvals:ApprovalEventCollection":{"name":"approvaleventcollection","className":"ApprovalEventCollection","qualifiedName":"@happyvertical/smrt-approvals:ApprovalEventCollection","collection":"approvalevents","filePath":"/home/runner/work/smrt/smrt/packages/approvals/src/collections/ApprovalEventCollection.ts","packageName":"@happyvertical/smrt-approvals","fields":{},"methods":{},"decoratorConfig":{"api":{"include":["list","get"]},"mcp":{"include":["list","get"]},"cli":false,"tableName":"approval_events"},"extends":"SmrtCollection","extendsTypeArg":"ApprovalEvent","exportName":"ApprovalEventCollection","collectionExportName":"ApprovalEventCollectionCollection","schema":{"tableName":"approval_events","ddl":"CREATE TABLE IF NOT EXISTS \\"approval_events\\" (\\n \\"id\\" UUID PRIMARY KEY NOT NULL,\\n \\"slug\\" TEXT NOT NULL,\\n \\"context\\" TEXT NOT NULL DEFAULT '',\\n \\"created_at\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\"updated_at\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\"tenant_id\\" UUID NOT NULL,\\n \\"request_id\\" UUID NOT NULL,\\n \\"type\\" TEXT NOT NULL,\\n \\"actor_id\\" TEXT NOT NULL,\\n \\"actor_type\\" TEXT,\\n \\"reason\\" TEXT,\\n \\"subject_revision_hash\\" TEXT,\\n \\"sequence\\" INTEGER,\\n \\"vote_key\\" TEXT,\\n \\"occurred_at\\" TIMESTAMP\\n);","columns":{"id":{"type":"UUID","primaryKey":true,"referenceKind":"id","notNull":true},"slug":{"type":"TEXT","notNull":true},"context":{"type":"TEXT","notNull":true,"default":""},"created_at":{"type":"TIMESTAMP","notNull":true,"default":"current_timestamp"},"updated_at":{"type":"TIMESTAMP","notNull":true,"default":"current_timestamp"},"tenant_id":{"type":"UUID","referenceKind":"tenantId","notNull":true,"unique":false},"request_id":{"type":"UUID","referenceKind":"foreignKey","notNull":true,"unique":false,"foreignKey":{"table":"approval_requests","column":"id","onDelete":"RESTRICT","onUpdate":"RESTRICT","engines":["postgres","sqlite"]}},"type":{"type":"TEXT","notNull":true,"unique":false},"actor_id":{"type":"TEXT","notNull":true,"unique":false},"actor_type":{"type":"TEXT","notNull":false,"unique":false},"reason":{"type":"TEXT","notNull":false,"unique":false},"subject_revision_hash":{"type":"TEXT","notNull":false,"unique":false},"sequence":{"type":"INTEGER","notNull":false,"unique":false},"vote_key":{"type":"TEXT","notNull":false,"unique":false},"occurred_at":{"type":"TIMESTAMP","notNull":false,"unique":false}},"indexes":[{"name":"approval_events_slug_context_idx","columns":["tenant_id","slug","context"],"unique":true},{"name":"approval_events_request_sequence_key","columns":["request_id","sequence"],"unique":true},{"name":"approval_events_request_vote_key","columns":["request_id","vote_key"],"unique":true},{"name":"approval_events_tenant_occurred_idx","columns":["tenant_id","occurred_at"]},{"name":"approval_events_tenant_id_created_at_idx","columns":["tenant_id","created_at"]}],"version":"4963053a"},"importPath":"@happyvertical/smrt-approvals"},"@happyvertical/smrt-approvals:ApprovalPolicyCollection":{"name":"approvalpolicycollection","className":"ApprovalPolicyCollection","qualifiedName":"@happyvertical/smrt-approvals:ApprovalPolicyCollection","collection":"approvalpolicies","filePath":"/home/runner/work/smrt/smrt/packages/approvals/src/collections/ApprovalPolicyCollection.ts","packageName":"@happyvertical/smrt-approvals","fields":{},"methods":{},"decoratorConfig":{"api":{"include":["list","get"]},"mcp":{"include":["list","get"]},"cli":false,"tableName":"approval_policies"},"extends":"SmrtCollection","extendsTypeArg":"ApprovalPolicy","exportName":"ApprovalPolicyCollection","collectionExportName":"ApprovalPolicyCollectionCollection","schema":{"tableName":"approval_policies","ddl":"CREATE TABLE IF NOT EXISTS \\"approval_policies\\" (\\n \\"id\\" UUID PRIMARY KEY NOT NULL,\\n \\"slug\\" TEXT NOT NULL,\\n \\"context\\" TEXT NOT NULL DEFAULT '',\\n \\"created_at\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\"updated_at\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\"tenant_id\\" UUID NOT NULL,\\n \\"kind\\" TEXT NOT NULL,\\n \\"required_approvals\\" INTEGER,\\n \\"ttl_ms\\" INTEGER,\\n \\"required_permission\\" TEXT,\\n \\"updated_by\\" TEXT\\n);","columns":{"id":{"type":"UUID","primaryKey":true,"referenceKind":"id","notNull":true},"slug":{"type":"TEXT","notNull":true},"context":{"type":"TEXT","notNull":true,"default":""},"created_at":{"type":"TIMESTAMP","notNull":true,"default":"current_timestamp"},"updated_at":{"type":"TIMESTAMP","notNull":true,"default":"current_timestamp"},"tenant_id":{"type":"UUID","referenceKind":"tenantId","notNull":true,"unique":false},"kind":{"type":"TEXT","notNull":true,"unique":false},"required_approvals":{"type":"INTEGER","notNull":false,"unique":false},"ttl_ms":{"type":"INTEGER","notNull":false,"unique":false},"required_permission":{"type":"TEXT","notNull":false,"unique":false},"updated_by":{"type":"TEXT","notNull":false,"unique":false}},"indexes":[{"name":"approval_policies_slug_context_idx","columns":["tenant_id","slug","context"],"unique":true},{"name":"approval_policies_tenant_kind_key","columns":["tenant_id","kind"],"unique":true},{"name":"approval_policies_tenant_id_created_at_idx","columns":["tenant_id","created_at"]}],"version":"80929cc8"},"importPath":"@happyvertical/smrt-approvals"},"@happyvertical/smrt-approvals:ApprovalRequestCollection":{"name":"approvalrequestcollection","className":"ApprovalRequestCollection","qualifiedName":"@happyvertical/smrt-approvals:ApprovalRequestCollection","collection":"approvalrequests","filePath":"/home/runner/work/smrt/smrt/packages/approvals/src/collections/ApprovalRequestCollection.ts","packageName":"@happyvertical/smrt-approvals","fields":{},"methods":{},"decoratorConfig":{"api":{"include":["list","get"]},"mcp":{"include":["list","get"]},"cli":false,"tableName":"approval_requests"},"extends":"SmrtCollection","extendsTypeArg":"ApprovalRequest","exportName":"ApprovalRequestCollection","collectionExportName":"ApprovalRequestCollectionCollection","schema":{"tableName":"approval_requests","ddl":"CREATE TABLE IF NOT EXISTS \\"approval_requests\\" (\\n \\"id\\" UUID PRIMARY KEY NOT NULL,\\n \\"slug\\" TEXT NOT NULL,\\n \\"context\\" TEXT NOT NULL DEFAULT '',\\n \\"created_at\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\"updated_at\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\"tenant_id\\" UUID NOT NULL,\\n \\"kind\\" TEXT NOT NULL,\\n \\"subject_type\\" TEXT NOT NULL,\\n \\"subject_id\\" TEXT NOT NULL,\\n \\"subject_revision_hash\\" TEXT NOT NULL,\\n \\"request_key\\" TEXT NOT NULL,\\n \\"requested_by\\" TEXT NOT NULL,\\n \\"requested_by_type\\" TEXT,\\n \\"summary\\" TEXT,\\n \\"status\\" TEXT,\\n \\"required_approvals\\" INTEGER,\\n \\"approval_count\\" INTEGER,\\n \\"required_permissions\\" TEXT,\\n \\"version\\" INTEGER,\\n \\"expires_at\\" TIMESTAMP,\\n \\"decided_at\\" TIMESTAMP,\\n \\"consumed_at\\" TIMESTAMP,\\n \\"consumed_by\\" TEXT\\n);","columns":{"id":{"type":"UUID","primaryKey":true,"referenceKind":"id","notNull":true},"slug":{"type":"TEXT","notNull":true},"context":{"type":"TEXT","notNull":true,"default":""},"created_at":{"type":"TIMESTAMP","notNull":true,"default":"current_timestamp"},"updated_at":{"type":"TIMESTAMP","notNull":true,"default":"current_timestamp"},"tenant_id":{"type":"UUID","referenceKind":"tenantId","notNull":true,"unique":false},"kind":{"type":"TEXT","notNull":true,"unique":false},"subject_type":{"type":"TEXT","notNull":true,"unique":false},"subject_id":{"type":"TEXT","notNull":true,"unique":false},"subject_revision_hash":{"type":"TEXT","notNull":true,"unique":false},"request_key":{"type":"TEXT","notNull":true,"unique":false},"requested_by":{"type":"TEXT","notNull":true,"unique":false},"requested_by_type":{"type":"TEXT","notNull":false,"unique":false},"summary":{"type":"TEXT","notNull":false,"unique":false},"status":{"type":"TEXT","notNull":false,"unique":false},"required_approvals":{"type":"INTEGER","notNull":false,"unique":false},"approval_count":{"type":"INTEGER","notNull":false,"unique":false},"required_permissions":{"type":"TEXT","notNull":false,"unique":false},"version":{"type":"INTEGER","notNull":false,"unique":false},"expires_at":{"type":"TIMESTAMP","notNull":false,"unique":false},"decided_at":{"type":"TIMESTAMP","notNull":false,"unique":false},"consumed_at":{"type":"TIMESTAMP","notNull":false,"unique":false},"consumed_by":{"type":"TEXT","notNull":false,"unique":false}},"indexes":[{"name":"approval_requests_slug_context_idx","columns":["tenant_id","slug","context"],"unique":true},{"name":"approval_requests_tenant_request_key","columns":["tenant_id","request_key"],"unique":true},{"name":"approval_requests_status_expiry_idx","columns":["tenant_id","status","expires_at"]},{"name":"approval_requests_subject_idx","columns":["tenant_id","subject_type","subject_id"]},{"name":"approval_requests_tenant_id_created_at_idx","columns":["tenant_id","created_at"]}],"version":"a5800bdd"},"importPath":"@happyvertical/smrt-approvals"},"@happyvertical/smrt-approvals:ApprovalEvent":{"name":"approvalevent","className":"ApprovalEvent","qualifiedName":"@happyvertical/smrt-approvals:ApprovalEvent","collection":"approvalevents","description":"One immutable entry in an approval request's history.","filePath":"/home/runner/work/smrt/smrt/packages/approvals/src/models/ApprovalEvent.ts","packageName":"@happyvertical/smrt-approvals","fields":{"tenantId":{"type":"text","required":true,"_meta":{"sqlType":"UUID","__tenancy":{"isTenantIdField":true,"autoFilter":true,"required":true,"autoPopulate":true,"nullable":false,"mode":"required","field":"tenantId","allowSuperAdminBypass":false}}},"requestId":{"type":"foreignKey","required":true,"related":"ApprovalRequest","_meta":{"required":true,"constraint":{"engines":["postgres","sqlite"]},"onDelete":"RESTRICT","onUpdate":"RESTRICT"}},"type":{"type":"text","required":true,"_meta":{"required":true,"readonly":true},"readonly":true},"actorId":{"type":"text","required":true,"_meta":{"required":true,"readonly":true},"readonly":true},"actorType":{"type":"text","required":false,"_meta":{"readonly":true},"enum":["human","agent","service"],"readonly":true},"reason":{"type":"text","required":false,"_meta":{"readonly":true},"readonly":true},"subjectRevisionHash":{"type":"text","required":false,"_meta":{"readonly":true},"readonly":true},"sequence":{"type":"integer","required":false,"_meta":{"readonly":true},"readonly":true},"voteKey":{"type":"text","required":false,"_meta":{"nullable":true,"readonly":true},"readonly":true},"occurredAt":{"type":"datetime","required":false,"_meta":{"readonly":true},"readonly":true}},"methods":{"save":{"name":"save","async":true,"parameters":[],"returnType":"Promise<this>","isStatic":false,"isPublic":true},"delete":{"name":"delete","async":true,"parameters":[],"returnType":"Promise<void>","isStatic":false,"isPublic":true}},"decoratorConfig":{"tableName":"approval_events","api":{"include":["list","get"]},"mcp":{"include":["list","get"]},"cli":false,"indexes":[{"name":"approval_events_request_sequence_key","columns":["requestId","sequence"],"unique":true},{"name":"approval_events_request_vote_key","columns":["requestId","voteKey"],"unique":true},{"name":"approval_events_tenant_occurred_idx","columns":["tenantId","occurredAt"]}],"tenantScoped":{"mode":"required"},"conflictColumns":["tenant_id","slug","context"]},"extends":"SmrtObject","exportName":"ApprovalEvent","collectionExportName":"ApprovalEventCollection","validationRules":[{"field":"requestId","rule":"required","fieldType":"foreignKey"},{"field":"type","rule":"required","fieldType":"text"},{"field":"actorId","rule":"required","fieldType":"text"}],"schema":{"tableName":"approval_events","ddl":"CREATE TABLE IF NOT EXISTS \\"approval_events\\" (\\n \\"id\\" UUID PRIMARY KEY NOT NULL,\\n \\"slug\\" TEXT NOT NULL,\\n \\"context\\" TEXT NOT NULL DEFAULT '',\\n \\"created_at\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\"updated_at\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\"tenant_id\\" UUID NOT NULL,\\n \\"request_id\\" UUID NOT NULL,\\n \\"type\\" TEXT NOT NULL,\\n \\"actor_id\\" TEXT NOT NULL,\\n \\"actor_type\\" TEXT,\\n \\"reason\\" TEXT,\\n \\"subject_revision_hash\\" TEXT,\\n \\"sequence\\" INTEGER,\\n \\"vote_key\\" TEXT,\\n \\"occurred_at\\" TIMESTAMP\\n);","columns":{"id":{"type":"UUID","primaryKey":true,"referenceKind":"id","notNull":true},"slug":{"type":"TEXT","notNull":true},"context":{"type":"TEXT","notNull":true,"default":""},"created_at":{"type":"TIMESTAMP","notNull":true,"default":"current_timestamp"},"updated_at":{"type":"TIMESTAMP","notNull":true,"default":"current_timestamp"},"tenant_id":{"type":"UUID","referenceKind":"tenantId","notNull":true,"unique":false},"request_id":{"type":"UUID","referenceKind":"foreignKey","notNull":true,"unique":false,"foreignKey":{"table":"approval_requests","column":"id","onDelete":"RESTRICT","onUpdate":"RESTRICT","engines":["postgres","sqlite"]}},"type":{"type":"TEXT","notNull":true,"unique":false},"actor_id":{"type":"TEXT","notNull":true,"unique":false},"actor_type":{"type":"TEXT","notNull":false,"unique":false},"reason":{"type":"TEXT","notNull":false,"unique":false},"subject_revision_hash":{"type":"TEXT","notNull":false,"unique":false},"sequence":{"type":"INTEGER","notNull":false,"unique":false},"vote_key":{"type":"TEXT","notNull":false,"unique":false},"occurred_at":{"type":"TIMESTAMP","notNull":false,"unique":false}},"indexes":[{"name":"approval_events_slug_context_idx","columns":["tenant_id","slug","context"],"unique":true},{"name":"approval_events_request_sequence_key","columns":["request_id","sequence"],"unique":true},{"name":"approval_events_request_vote_key","columns":["request_id","vote_key"],"unique":true},{"name":"approval_events_tenant_occurred_idx","columns":["tenant_id","occurred_at"]},{"name":"approval_events_tenant_id_created_at_idx","columns":["tenant_id","created_at"]}],"version":"4963053a"},"importPath":"@happyvertical/smrt-approvals"},"@happyvertical/smrt-approvals:ApprovalPolicy":{"name":"approvalpolicy","className":"ApprovalPolicy","qualifiedName":"@happyvertical/smrt-approvals:ApprovalPolicy","collection":"approvalpolicies","description":"Tenant-level tightening of an approval kind. One row per tenant and kind.","filePath":"/home/runner/work/smrt/smrt/packages/approvals/src/models/ApprovalPolicy.ts","packageName":"@happyvertical/smrt-approvals","fields":{"tenantId":{"type":"text","required":true,"_meta":{"sqlType":"UUID","__tenancy":{"isTenantIdField":true,"autoFilter":true,"required":true,"autoPopulate":true,"nullable":false,"mode":"required","field":"tenantId","allowSuperAdminBypass":false}}},"kind":{"type":"text","required":true,"_meta":{"required":true}},"requiredApprovals":{"type":"integer","required":false,"_meta":{}},"ttlMs":{"type":"integer","required":false,"_meta":{}},"requiredPermission":{"type":"text","required":false,"_meta":{}},"updatedBy":{"type":"text","required":false,"_meta":{}}},"methods":{"save":{"name":"save","async":true,"parameters":[],"returnType":"Promise<this>","isStatic":false,"isPublic":true}},"decoratorConfig":{"tableName":"approval_policies","api":{"include":["list","get"]},"mcp":{"include":["list","get"]},"cli":false,"indexes":[{"name":"approval_policies_tenant_kind_key","columns":["tenantId","kind"],"unique":true}],"tenantScoped":{"mode":"required"},"conflictColumns":["tenant_id","slug","context"]},"extends":"SmrtObject","exportName":"ApprovalPolicy","collectionExportName":"ApprovalPolicyCollection","validationRules":[{"field":"kind","rule":"required","fieldType":"text"}],"schema":{"tableName":"approval_policies","ddl":"CREATE TABLE IF NOT EXISTS \\"approval_policies\\" (\\n \\"id\\" UUID PRIMARY KEY NOT NULL,\\n \\"slug\\" TEXT NOT NULL,\\n \\"context\\" TEXT NOT NULL DEFAULT '',\\n \\"created_at\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\"updated_at\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\"tenant_id\\" UUID NOT NULL,\\n \\"kind\\" TEXT NOT NULL,\\n \\"required_approvals\\" INTEGER,\\n \\"ttl_ms\\" INTEGER,\\n \\"required_permission\\" TEXT,\\n \\"updated_by\\" TEXT\\n);","columns":{"id":{"type":"UUID","primaryKey":true,"referenceKind":"id","notNull":true},"slug":{"type":"TEXT","notNull":true},"context":{"type":"TEXT","notNull":true,"default":""},"created_at":{"type":"TIMESTAMP","notNull":true,"default":"current_timestamp"},"updated_at":{"type":"TIMESTAMP","notNull":true,"default":"current_timestamp"},"tenant_id":{"type":"UUID","referenceKind":"tenantId","notNull":true,"unique":false},"kind":{"type":"TEXT","notNull":true,"unique":false},"required_approvals":{"type":"INTEGER","notNull":false,"unique":false},"ttl_ms":{"type":"INTEGER","notNull":false,"unique":false},"required_permission":{"type":"TEXT","notNull":false,"unique":false},"updated_by":{"type":"TEXT","notNull":false,"unique":false}},"indexes":[{"name":"approval_policies_slug_context_idx","columns":["tenant_id","slug","context"],"unique":true},{"name":"approval_policies_tenant_kind_key","columns":["tenant_id","kind"],"unique":true},{"name":"approval_policies_tenant_id_created_at_idx","columns":["tenant_id","created_at"]}],"version":"80929cc8"},"importPath":"@happyvertical/smrt-approvals"},"@happyvertical/smrt-approvals:ApprovalRequest":{"name":"approvalrequest","className":"ApprovalRequest","qualifiedName":"@happyvertical/smrt-approvals:ApprovalRequest","collection":"approvalrequests","description":"A request for approval of one subject revision.","filePath":"/home/runner/work/smrt/smrt/packages/approvals/src/models/ApprovalRequest.ts","packageName":"@happyvertical/smrt-approvals","fields":{"tenantId":{"type":"text","required":true,"_meta":{"sqlType":"UUID","__tenancy":{"isTenantIdField":true,"autoFilter":true,"required":true,"autoPopulate":true,"nullable":false,"mode":"required","field":"tenantId","allowSuperAdminBypass":false}}},"kind":{"type":"text","required":true,"_meta":{"required":true,"readonly":true},"readonly":true},"subjectType":{"type":"text","required":true,"_meta":{"required":true,"readonly":true},"readonly":true},"subjectId":{"type":"text","required":true,"_meta":{"required":true,"readonly":true},"readonly":true},"subjectRevisionHash":{"type":"text","required":true,"_meta":{"required":true,"readonly":true},"readonly":true},"requestKey":{"type":"text","required":true,"_meta":{"required":true,"readonly":true},"readonly":true},"requestedBy":{"type":"text","required":true,"_meta":{"required":true,"readonly":true},"readonly":true},"requestedByType":{"type":"text","required":false,"_meta":{"readonly":true},"enum":["human","agent","service"],"readonly":true},"summary":{"type":"text","required":false,"_meta":{"readonly":true},"readonly":true},"status":{"type":"text","required":false,"_meta":{"readonly":true},"readonly":true},"requiredApprovals":{"type":"integer","required":false,"_meta":{"readonly":true},"readonly":true},"approvalCount":{"type":"integer","required":false,"_meta":{"readonly":true},"readonly":true},"requiredPermissions":{"type":"text","required":false,"_meta":{"readonly":true},"readonly":true},"version":{"type":"integer","required":false,"_meta":{"readonly":true},"readonly":true},"expiresAt":{"type":"datetime","required":false,"_meta":{"readonly":true},"readonly":true},"decidedAt":{"type":"datetime","required":false,"_meta":{"readonly":true},"readonly":true},"consumedAt":{"type":"datetime","required":false,"_meta":{"readonly":true},"readonly":true},"consumedBy":{"type":"text","required":false,"_meta":{"readonly":true},"readonly":true}},"methods":{"getRequiredPermissions":{"name":"getRequiredPermissions","async":false,"parameters":[],"returnType":"string[]","isStatic":false,"isPublic":true},"setRequiredPermissions":{"name":"setRequiredPermissions","async":false,"parameters":[{"name":"slugs","type":"any","optional":false,"typeUnresolved":true}],"returnType":"void","isStatic":false,"isPublic":true},"expiresAtMs":{"name":"expiresAtMs","async":false,"parameters":[],"returnType":"number","isStatic":false,"isPublic":true},"isConsumed":{"name":"isConsumed","async":false,"parameters":[],"returnType":"boolean","isStatic":false,"isPublic":true},"save":{"name":"save","async":true,"parameters":[],"returnType":"Promise<this>","isStatic":false,"isPublic":true},"delete":{"name":"delete","async":true,"parameters":[],"returnType":"Promise<void>","isStatic":false,"isPublic":true}},"decoratorConfig":{"tableName":"approval_requests","api":{"include":["list","get"]},"mcp":{"include":["list","get"]},"cli":false,"indexes":[{"name":"approval_requests_tenant_request_key","columns":["tenantId","requestKey"],"unique":true},{"name":"approval_requests_status_expiry_idx","columns":["tenantId","status","expiresAt"]},{"name":"approval_requests_subject_idx","columns":["tenantId","subjectType","subjectId"]}],"tenantScoped":{"mode":"required"},"conflictColumns":["tenant_id","slug","context"]},"extends":"SmrtObject","exportName":"ApprovalRequest","collectionExportName":"ApprovalRequestCollection","validationRules":[{"field":"kind","rule":"required","fieldType":"text"},{"field":"subjectType","rule":"required","fieldType":"text"},{"field":"subjectId","rule":"required","fieldType":"text"},{"field":"subjectRevisionHash","rule":"required","fieldType":"text"},{"field":"requestKey","rule":"required","fieldType":"text"},{"field":"requestedBy","rule":"required","fieldType":"text"}],"schema":{"tableName":"approval_requests","ddl":"CREATE TABLE IF NOT EXISTS \\"approval_requests\\" (\\n \\"id\\" UUID PRIMARY KEY NOT NULL,\\n \\"slug\\" TEXT NOT NULL,\\n \\"context\\" TEXT NOT NULL DEFAULT '',\\n \\"created_at\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\"updated_at\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\"tenant_id\\" UUID NOT NULL,\\n \\"kind\\" TEXT NOT NULL,\\n \\"subject_type\\" TEXT NOT NULL,\\n \\"subject_id\\" TEXT NOT NULL,\\n \\"subject_revision_hash\\" TEXT NOT NULL,\\n \\"request_key\\" TEXT NOT NULL,\\n \\"requested_by\\" TEXT NOT NULL,\\n \\"requested_by_type\\" TEXT,\\n \\"summary\\" TEXT,\\n \\"status\\" TEXT,\\n \\"required_approvals\\" INTEGER,\\n \\"approval_count\\" INTEGER,\\n \\"required_permissions\\" TEXT,\\n \\"version\\" INTEGER,\\n \\"expires_at\\" TIMESTAMP,\\n \\"decided_at\\" TIMESTAMP,\\n \\"consumed_at\\" TIMESTAMP,\\n \\"consumed_by\\" TEXT\\n);","columns":{"id":{"type":"UUID","primaryKey":true,"referenceKind":"id","notNull":true},"slug":{"type":"TEXT","notNull":true},"context":{"type":"TEXT","notNull":true,"default":""},"created_at":{"type":"TIMESTAMP","notNull":true,"default":"current_timestamp"},"updated_at":{"type":"TIMESTAMP","notNull":true,"default":"current_timestamp"},"tenant_id":{"type":"UUID","referenceKind":"tenantId","notNull":true,"unique":false},"kind":{"type":"TEXT","notNull":true,"unique":false},"subject_type":{"type":"TEXT","notNull":true,"unique":false},"subject_id":{"type":"TEXT","notNull":true,"unique":false},"subject_revision_hash":{"type":"TEXT","notNull":true,"unique":false},"request_key":{"type":"TEXT","notNull":true,"unique":false},"requested_by":{"type":"TEXT","notNull":true,"unique":false},"requested_by_type":{"type":"TEXT","notNull":false,"unique":false},"summary":{"type":"TEXT","notNull":false,"unique":false},"status":{"type":"TEXT","notNull":false,"unique":false},"required_approvals":{"type":"INTEGER","notNull":false,"unique":false},"approval_count":{"type":"INTEGER","notNull":false,"unique":false},"required_permissions":{"type":"TEXT","notNull":false,"unique":false},"version":{"type":"INTEGER","notNull":false,"unique":false},"expires_at":{"type":"TIMESTAMP","notNull":false,"unique":false},"decided_at":{"type":"TIMESTAMP","notNull":false,"unique":false},"consumed_at":{"type":"TIMESTAMP","notNull":false,"unique":false},"consumed_by":{"type":"TEXT","notNull":false,"unique":false}},"indexes":[{"name":"approval_requests_slug_context_idx","columns":["tenant_id","slug","context"],"unique":true},{"name":"approval_requests_tenant_request_key","columns":["tenant_id","request_key"],"unique":true},{"name":"approval_requests_status_expiry_idx","columns":["tenant_id","status","expires_at"]},{"name":"approval_requests_subject_idx","columns":["tenant_id","subject_type","subject_id"]},{"name":"approval_requests_tenant_id_created_at_idx","columns":["tenant_id","created_at"]}],"version":"a5800bdd"},"importPath":"@happyvertical/smrt-approvals"}},"moduleType":"smrt","smrtDependencies":["@happyvertical/smrt-core","@happyvertical/smrt-tenancy","@happyvertical/smrt-users"],"browser":{"status":"server-only","issues":["#3617","#3624","#3626"],"via":["@happyvertical/smrt-profiles","@happyvertical/smrt-tenancy","@happyvertical/smrt-users"]},"stampsConstructors":true}`));
7
+ //#endregion
8
+ //#region src/permissions.ts
9
+ var CANCEL_ANY_APPROVAL_PERMISSION = "approvals.cancel-any";
10
+ var MANAGE_APPROVAL_POLICY_PERMISSION = "approvals.manage-policy";
11
+ var APPROVAL_PERMISSION_DEFINITIONS = [{
12
+ slug: CANCEL_ANY_APPROVAL_PERMISSION,
13
+ category: "approvals",
14
+ name: "Cancel Any Approval Request",
15
+ description: "Cancel a pending approval request made by someone else in the tenant"
16
+ }, {
17
+ slug: MANAGE_APPROVAL_POLICY_PERMISSION,
18
+ category: "approvals",
19
+ name: "Manage Approval Policy",
20
+ description: "Tighten an approval kind's quorum, expiry, or required permission for the tenant"
21
+ }];
22
+ var registered = false;
23
+ function registerApprovalPermissions() {
24
+ return registerPermissionDefinitions(APPROVAL_PERMISSION_DEFINITIONS);
25
+ }
26
+ function ensureApprovalPermissionsRegistered() {
27
+ if (registered) return;
28
+ registered = true;
29
+ registerApprovalPermissions();
30
+ }
31
+ //#endregion
32
+ //#region src/write-capability.ts
33
+ var SERVICE_WRITE = /* @__PURE__ */ Symbol("smrt-approvals.serviceWrite");
34
+ var TOKEN = Object.freeze({ capability: "smrt-approvals.serviceWrite" });
35
+ var minted = /* @__PURE__ */ new WeakSet();
36
+ function serviceWriteOption() {
37
+ return { [SERVICE_WRITE]: TOKEN };
38
+ }
39
+ function adoptServiceWrite(instance, options) {
40
+ if (options && typeof options === "object" && options[SERVICE_WRITE] === TOKEN) minted.add(instance);
41
+ }
42
+ function hasServiceWrite(instance) {
43
+ return minted.has(instance);
44
+ }
45
+ //#endregion
46
+ //#region src/models/ApprovalRequest.ts
47
+ var __defProp$5 = Object.defineProperty;
48
+ var __getOwnPropDesc$5 = Object.getOwnPropertyDescriptor;
49
+ var __defNormalProp$5 = (obj, key, value) => key in obj ? __defProp$5(obj, key, {
50
+ enumerable: true,
51
+ configurable: true,
52
+ writable: true,
53
+ value
54
+ }) : obj[key] = value;
55
+ var __decorateClass$5 = (decorators, target, key, kind) => {
56
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$5(target, key) : target;
57
+ for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
58
+ if (kind && result) __defProp$5(target, key, result);
59
+ return result;
60
+ };
61
+ var __publicField$5 = (obj, key, value) => __defNormalProp$5(obj, typeof key !== "symbol" ? key + "" : key, value);
62
+ var ApprovalRequest = class extends SmrtObject {
63
+ tenantId = "";
64
+ kind = "";
65
+ subjectType = "";
66
+ subjectId = "";
67
+ subjectRevisionHash = "";
68
+ requestKey = "";
69
+ requestedBy = "";
70
+ requestedByType = "human";
71
+ summary = "";
72
+ status = "pending";
73
+ requiredApprovals = 1;
74
+ approvalCount = 0;
75
+ requiredPermissions = "[]";
76
+ version = 0;
77
+ expiresAt = null;
78
+ decidedAt = null;
79
+ consumedAt = null;
80
+ consumedBy = "";
81
+ constructor(options = {}) {
82
+ super(options);
83
+ adoptServiceWrite(this, options);
84
+ if (options.tenantId !== void 0) this.tenantId = options.tenantId;
85
+ if (options.kind !== void 0) this.kind = options.kind;
86
+ if (options.subjectType !== void 0) this.subjectType = options.subjectType;
87
+ if (options.subjectId !== void 0) this.subjectId = options.subjectId;
88
+ if (options.subjectRevisionHash !== void 0) this.subjectRevisionHash = options.subjectRevisionHash;
89
+ if (options.requestKey !== void 0) this.requestKey = options.requestKey;
90
+ if (options.requestedBy !== void 0) this.requestedBy = options.requestedBy;
91
+ if (options.requestedByType !== void 0) this.requestedByType = options.requestedByType;
92
+ if (options.summary !== void 0) this.summary = options.summary;
93
+ if (options.status !== void 0) this.status = options.status;
94
+ if (options.requiredApprovals !== void 0) this.requiredApprovals = options.requiredApprovals;
95
+ if (options.approvalCount !== void 0) this.approvalCount = options.approvalCount;
96
+ if (options.requiredPermissions !== void 0) this.requiredPermissions = options.requiredPermissions;
97
+ if (options.version !== void 0) this.version = options.version;
98
+ if (options.expiresAt !== void 0) this.expiresAt = options.expiresAt;
99
+ if (options.decidedAt !== void 0) this.decidedAt = options.decidedAt;
100
+ if (options.consumedAt !== void 0) this.consumedAt = options.consumedAt;
101
+ if (options.consumedBy !== void 0) this.consumedBy = options.consumedBy;
102
+ }
103
+ /** The permission slugs a decider must hold (never throws; bad JSON = none valid). */
104
+ getRequiredPermissions() {
105
+ try {
106
+ const parsed = JSON.parse(this.requiredPermissions || "[]");
107
+ return Array.isArray(parsed) ? parsed.filter((slug) => typeof slug === "string") : [];
108
+ } catch {
109
+ return [];
110
+ }
111
+ }
112
+ /** Store the permission slugs a decider must hold. */
113
+ setRequiredPermissions(slugs) {
114
+ this.requiredPermissions = JSON.stringify([...slugs]);
115
+ }
116
+ /** The expiry as epoch milliseconds (`NaN` when unset or unparseable). */
117
+ expiresAtMs() {
118
+ const value = this.expiresAt;
119
+ if (value instanceof Date) return value.getTime();
120
+ if (typeof value === "string" || typeof value === "number") return new Date(value).getTime();
121
+ return NaN;
122
+ }
123
+ /** True when the approval was consumed. */
124
+ isConsumed() {
125
+ return this.consumedAt !== null && this.consumedAt !== void 0;
126
+ }
127
+ /**
128
+ * Refuse any insert `ApprovalService` did not mint (see
129
+ * `write-capability.ts`), and any minted insert that does not start in the
130
+ * initial state. Runs for ordinary saves and bulk creates alike (core
131
+ * wraps an error thrown here; `save()` checks first so callers see it).
132
+ *
133
+ * @throws {ApprovalError} `APPROVAL_FORBIDDEN`.
134
+ */
135
+ async validateBeforeSave() {
136
+ await super.validateBeforeSave();
137
+ this.assertInsertAllowed();
138
+ }
139
+ assertInsertAllowed() {
140
+ if (this.isPersisted) return;
141
+ if (!hasServiceWrite(this)) throw new ApprovalError("APPROVAL_FORBIDDEN", "ApprovalRequest rows are created only by ApprovalService.requestApproval().");
142
+ if (this.status !== "pending" || Number(this.approvalCount) !== 0 || Number(this.version) !== 1 || this.decidedAt || this.consumedAt || this.consumedBy) throw new ApprovalError("APPROVAL_FORBIDDEN", "A new ApprovalRequest must start pending, undecided, and unconsumed.");
143
+ }
144
+ /**
145
+ * Insert a new request. A loaded request is never re-saved: every later
146
+ * change is a guarded transition in `ApprovalService`.
147
+ *
148
+ * @throws {ApprovalError} `APPROVAL_FORBIDDEN` for a persisted request.
149
+ */
150
+ async save() {
151
+ if (this.isPersisted) throw new ApprovalError("APPROVAL_FORBIDDEN", `ApprovalRequest ${this.id} is changed only through ApprovalService transitions.`);
152
+ this.assertInsertAllowed();
153
+ this.requireInsertOnSave();
154
+ return await super.save();
155
+ }
156
+ /**
157
+ * Requests are durable records of a decision.
158
+ *
159
+ * @throws {ApprovalError} always.
160
+ */
161
+ async delete() {
162
+ throw new ApprovalError("APPROVAL_FORBIDDEN", `ApprovalRequest ${this.id} cannot be deleted.`);
163
+ }
164
+ };
165
+ __publicField$5(ApprovalRequest, "__smrtPackage__", "@happyvertical/smrt-approvals");
166
+ __decorateClass$5([tenantId()], ApprovalRequest.prototype, "tenantId", 2);
167
+ __decorateClass$5([field({
168
+ type: "text",
169
+ required: true,
170
+ readonly: true
171
+ })], ApprovalRequest.prototype, "kind", 2);
172
+ __decorateClass$5([field({
173
+ type: "text",
174
+ required: true,
175
+ readonly: true
176
+ })], ApprovalRequest.prototype, "subjectType", 2);
177
+ __decorateClass$5([field({
178
+ type: "text",
179
+ required: true,
180
+ readonly: true
181
+ })], ApprovalRequest.prototype, "subjectId", 2);
182
+ __decorateClass$5([field({
183
+ type: "text",
184
+ required: true,
185
+ readonly: true
186
+ })], ApprovalRequest.prototype, "subjectRevisionHash", 2);
187
+ __decorateClass$5([field({
188
+ type: "text",
189
+ required: true,
190
+ readonly: true
191
+ })], ApprovalRequest.prototype, "requestKey", 2);
192
+ __decorateClass$5([field({
193
+ type: "text",
194
+ required: true,
195
+ readonly: true
196
+ })], ApprovalRequest.prototype, "requestedBy", 2);
197
+ __decorateClass$5([field({
198
+ type: "text",
199
+ readonly: true
200
+ })], ApprovalRequest.prototype, "requestedByType", 2);
201
+ __decorateClass$5([field({
202
+ type: "text",
203
+ readonly: true
204
+ })], ApprovalRequest.prototype, "summary", 2);
205
+ __decorateClass$5([field({
206
+ type: "text",
207
+ readonly: true
208
+ })], ApprovalRequest.prototype, "status", 2);
209
+ __decorateClass$5([field({
210
+ type: "integer",
211
+ readonly: true
212
+ })], ApprovalRequest.prototype, "requiredApprovals", 2);
213
+ __decorateClass$5([field({
214
+ type: "integer",
215
+ readonly: true
216
+ })], ApprovalRequest.prototype, "approvalCount", 2);
217
+ __decorateClass$5([field({
218
+ type: "text",
219
+ readonly: true
220
+ })], ApprovalRequest.prototype, "requiredPermissions", 2);
221
+ __decorateClass$5([field({
222
+ type: "integer",
223
+ readonly: true
224
+ })], ApprovalRequest.prototype, "version", 2);
225
+ __decorateClass$5([field({
226
+ type: "datetime",
227
+ readonly: true
228
+ })], ApprovalRequest.prototype, "expiresAt", 2);
229
+ __decorateClass$5([field({
230
+ type: "datetime",
231
+ readonly: true
232
+ })], ApprovalRequest.prototype, "decidedAt", 2);
233
+ __decorateClass$5([field({
234
+ type: "datetime",
235
+ readonly: true
236
+ })], ApprovalRequest.prototype, "consumedAt", 2);
237
+ __decorateClass$5([field({
238
+ type: "text",
239
+ readonly: true
240
+ })], ApprovalRequest.prototype, "consumedBy", 2);
241
+ ApprovalRequest = __decorateClass$5([TenantScoped({ mode: "required" }), smrt({
242
+ tableName: "approval_requests",
243
+ api: { include: ["list", "get"] },
244
+ mcp: { include: ["list", "get"] },
245
+ cli: false,
246
+ indexes: [
247
+ {
248
+ name: "approval_requests_tenant_request_key",
249
+ columns: ["tenantId", "requestKey"],
250
+ unique: true
251
+ },
252
+ {
253
+ name: "approval_requests_status_expiry_idx",
254
+ columns: [
255
+ "tenantId",
256
+ "status",
257
+ "expiresAt"
258
+ ]
259
+ },
260
+ {
261
+ name: "approval_requests_subject_idx",
262
+ columns: [
263
+ "tenantId",
264
+ "subjectType",
265
+ "subjectId"
266
+ ]
267
+ }
268
+ ]
269
+ })], ApprovalRequest);
270
+ //#endregion
271
+ //#region src/models/ApprovalEvent.ts
272
+ var __defProp$4 = Object.defineProperty;
273
+ var __getOwnPropDesc$4 = Object.getOwnPropertyDescriptor;
274
+ var __defNormalProp$4 = (obj, key, value) => key in obj ? __defProp$4(obj, key, {
275
+ enumerable: true,
276
+ configurable: true,
277
+ writable: true,
278
+ value
279
+ }) : obj[key] = value;
280
+ var __decorateClass$4 = (decorators, target, key, kind) => {
281
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$4(target, key) : target;
282
+ for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
283
+ if (kind && result) __defProp$4(target, key, result);
284
+ return result;
285
+ };
286
+ var __publicField$4 = (obj, key, value) => __defNormalProp$4(obj, typeof key !== "symbol" ? key + "" : key, value);
287
+ var ApprovalEvent = class extends SmrtObject {
288
+ tenantId = "";
289
+ requestId = "";
290
+ type = "created";
291
+ actorId = "";
292
+ actorType = "human";
293
+ reason = "";
294
+ subjectRevisionHash = "";
295
+ sequence = 0;
296
+ voteKey = null;
297
+ occurredAt = null;
298
+ constructor(options = {}) {
299
+ super(options);
300
+ adoptServiceWrite(this, options);
301
+ if (options.tenantId !== void 0) this.tenantId = options.tenantId;
302
+ if (options.requestId !== void 0) this.requestId = options.requestId;
303
+ if (options.type !== void 0) this.type = options.type;
304
+ if (options.actorId !== void 0) this.actorId = options.actorId;
305
+ if (options.actorType !== void 0) this.actorType = options.actorType;
306
+ if (options.reason !== void 0) this.reason = options.reason;
307
+ if (options.subjectRevisionHash !== void 0) this.subjectRevisionHash = options.subjectRevisionHash;
308
+ if (options.sequence !== void 0) this.sequence = options.sequence;
309
+ if (options.voteKey !== void 0) this.voteKey = options.voteKey;
310
+ if (options.occurredAt !== void 0) this.occurredAt = options.occurredAt;
311
+ }
312
+ /**
313
+ * Append the event. An event is written once and never updated.
314
+ *
315
+ * @throws {ApprovalError} `APPROVAL_FORBIDDEN` for a persisted event.
316
+ */
317
+ async save() {
318
+ if (this.isPersisted) throw new ApprovalError("APPROVAL_FORBIDDEN", `ApprovalEvent ${this.id} is append-only and cannot be updated.`);
319
+ this.assertInsertAllowed();
320
+ if (!this.occurredAt) this.occurredAt = /* @__PURE__ */ new Date();
321
+ this.requireInsertOnSave();
322
+ return await super.save();
323
+ }
324
+ /**
325
+ * Refuse any insert `ApprovalService` did not mint in the same transaction
326
+ * as its guarded UPDATE (see `write-capability.ts`): a forged `approved`
327
+ * event would otherwise count toward quorum. Runs for ordinary saves and
328
+ * bulk creates alike.
329
+ *
330
+ * @throws {ApprovalError} `APPROVAL_FORBIDDEN`.
331
+ */
332
+ async validateBeforeSave() {
333
+ await super.validateBeforeSave();
334
+ this.assertInsertAllowed();
335
+ }
336
+ assertInsertAllowed() {
337
+ if (this.isPersisted || hasServiceWrite(this)) return;
338
+ throw new ApprovalError("APPROVAL_FORBIDDEN", "ApprovalEvent rows are appended only by ApprovalService transitions.");
339
+ }
340
+ /**
341
+ * Events are append-only.
342
+ *
343
+ * @throws {ApprovalError} always.
344
+ */
345
+ async delete() {
346
+ throw new ApprovalError("APPROVAL_FORBIDDEN", `ApprovalEvent ${this.id} is append-only and cannot be deleted.`);
347
+ }
348
+ };
349
+ __publicField$4(ApprovalEvent, "__smrtPackage__", "@happyvertical/smrt-approvals");
350
+ __decorateClass$4([tenantId()], ApprovalEvent.prototype, "tenantId", 2);
351
+ __decorateClass$4([foreignKey(ApprovalRequest, {
352
+ required: true,
353
+ readonly: true,
354
+ onDelete: "RESTRICT",
355
+ onUpdate: "RESTRICT",
356
+ constraint: { engines: ["postgres", "sqlite"] }
357
+ })], ApprovalEvent.prototype, "requestId", 2);
358
+ __decorateClass$4([field({
359
+ type: "text",
360
+ required: true,
361
+ readonly: true
362
+ })], ApprovalEvent.prototype, "type", 2);
363
+ __decorateClass$4([field({
364
+ type: "text",
365
+ required: true,
366
+ readonly: true
367
+ })], ApprovalEvent.prototype, "actorId", 2);
368
+ __decorateClass$4([field({
369
+ type: "text",
370
+ readonly: true
371
+ })], ApprovalEvent.prototype, "actorType", 2);
372
+ __decorateClass$4([field({
373
+ type: "text",
374
+ readonly: true
375
+ })], ApprovalEvent.prototype, "reason", 2);
376
+ __decorateClass$4([field({
377
+ type: "text",
378
+ readonly: true
379
+ })], ApprovalEvent.prototype, "subjectRevisionHash", 2);
380
+ __decorateClass$4([field({
381
+ type: "integer",
382
+ readonly: true
383
+ })], ApprovalEvent.prototype, "sequence", 2);
384
+ __decorateClass$4([field({
385
+ type: "text",
386
+ nullable: true,
387
+ readonly: true
388
+ })], ApprovalEvent.prototype, "voteKey", 2);
389
+ __decorateClass$4([field({
390
+ type: "datetime",
391
+ readonly: true
392
+ })], ApprovalEvent.prototype, "occurredAt", 2);
393
+ ApprovalEvent = __decorateClass$4([TenantScoped({ mode: "required" }), smrt({
394
+ tableName: "approval_events",
395
+ api: { include: ["list", "get"] },
396
+ mcp: { include: ["list", "get"] },
397
+ cli: false,
398
+ indexes: [
399
+ {
400
+ name: "approval_events_request_sequence_key",
401
+ columns: ["requestId", "sequence"],
402
+ unique: true
403
+ },
404
+ {
405
+ name: "approval_events_request_vote_key",
406
+ columns: ["requestId", "voteKey"],
407
+ unique: true
408
+ },
409
+ {
410
+ name: "approval_events_tenant_occurred_idx",
411
+ columns: ["tenantId", "occurredAt"]
412
+ }
413
+ ]
414
+ })], ApprovalEvent);
415
+ //#endregion
416
+ //#region src/collections/ApprovalEventCollection.ts
417
+ var __defProp$3 = Object.defineProperty;
418
+ var __getOwnPropDesc$3 = Object.getOwnPropertyDescriptor;
419
+ var __defNormalProp$3 = (obj, key, value) => key in obj ? __defProp$3(obj, key, {
420
+ enumerable: true,
421
+ configurable: true,
422
+ writable: true,
423
+ value
424
+ }) : obj[key] = value;
425
+ var __decorateClass$3 = (decorators, target, key, kind) => {
426
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$3(target, key) : target;
427
+ for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
428
+ if (kind && result) __defProp$3(target, key, result);
429
+ return result;
430
+ };
431
+ var __publicField$3 = (obj, key, value) => __defNormalProp$3(obj, typeof key !== "symbol" ? key + "" : key, value);
432
+ var ApprovalEventCollection = class extends SmrtCollection {};
433
+ __publicField$3(ApprovalEventCollection, "__smrtPackage__", "@happyvertical/smrt-approvals");
434
+ __publicField$3(ApprovalEventCollection, "_itemClass", ApprovalEvent);
435
+ ApprovalEventCollection = __decorateClass$3([smrt({
436
+ api: { include: ["list", "get"] },
437
+ mcp: { include: ["list", "get"] },
438
+ cli: false
439
+ })], ApprovalEventCollection);
440
+ //#endregion
441
+ //#region src/kinds.ts
442
+ var MAX_REQUIRED_APPROVALS = 25;
443
+ var DEFAULT_APPROVAL_TTL_MS = 10080 * 60 * 1e3;
444
+ var MAX_APPROVAL_TTL_MS = 366 * 24 * 60 * 60 * 1e3;
445
+ var MIN_APPROVAL_TTL_MS = 60 * 1e3;
446
+ var KIND_KEY = /^[a-z][a-z0-9-]*(?:\.[a-z][a-z0-9_-]*)+$/;
447
+ var QUALIFIED_CLASS = /^@[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._-]*:[A-Za-z_$][\w$]*$/;
448
+ var PERMISSION_SLUG = /^[a-z][a-z0-9_-]*(?:\.[a-z0-9][a-z0-9_-]*)+$/;
449
+ var kinds = /* @__PURE__ */ new Map();
450
+ function isPermissionSlug(value) {
451
+ return typeof value === "string" && PERMISSION_SLUG.test(value);
452
+ }
453
+ function assertQuorum(value, label) {
454
+ if (!Number.isSafeInteger(value) || value < 1 || value > 25) throw new ApprovalError("APPROVAL_INVALID", `${label} must be an integer from 1 to 25, got ${String(value)}.`);
455
+ }
456
+ function assertTtl(value, label) {
457
+ if (!Number.isSafeInteger(value) || value < 6e4 || value > 316224e5) throw new ApprovalError("APPROVAL_INVALID", `${label} must be an integer number of milliseconds from ${MIN_APPROVAL_TTL_MS} to ${MAX_APPROVAL_TTL_MS}, got ${String(value)}.`);
458
+ }
459
+ function defineApprovalKind(input) {
460
+ if (!KIND_KEY.test(input.key ?? "")) throw new ApprovalError("APPROVAL_INVALID", `Approval kind key must be dotted lower case such as 'social.post.publish', got '${String(input.key)}'.`);
461
+ if (!QUALIFIED_CLASS.test(input.subject ?? "")) throw new ApprovalError("APPROVAL_INVALID", `Approval kind '${input.key}' subject must be a qualified class name such as '@happyvertical/smrt-social:SocialPost', got '${String(input.subject)}'.`);
462
+ if (!isPermissionSlug(input.permission)) throw new ApprovalError("APPROVAL_INVALID", `Approval kind '${input.key}' permission must be a slug such as 'social.approve-post', got '${String(input.permission)}'.`);
463
+ const requiredApprovals = input.defaults?.requiredApprovals ?? 1;
464
+ const ttlMs = input.defaults?.ttlMs ?? 6048e5;
465
+ assertQuorum(requiredApprovals, `Approval kind '${input.key}' requiredApprovals`);
466
+ assertTtl(ttlMs, `Approval kind '${input.key}' ttlMs`);
467
+ const kind = Object.freeze({
468
+ key: input.key,
469
+ subject: input.subject,
470
+ permission: input.permission,
471
+ defaults: Object.freeze({
472
+ requiredApprovals,
473
+ ttlMs
474
+ })
475
+ });
476
+ const existing = kinds.get(kind.key);
477
+ if (existing) {
478
+ if (existing.subject === kind.subject && existing.permission === kind.permission && existing.defaults.requiredApprovals === requiredApprovals && existing.defaults.ttlMs === ttlMs) return existing;
479
+ throw new ApprovalError("APPROVAL_KIND_CONFLICT", `Approval kind '${kind.key}' is already defined with different rules.`);
480
+ }
481
+ kinds.set(kind.key, kind);
482
+ return kind;
483
+ }
484
+ function getApprovalKind(key) {
485
+ return kinds.get(key);
486
+ }
487
+ function listApprovalKinds() {
488
+ return [...kinds.values()].sort((a, b) => a.key.localeCompare(b.key));
489
+ }
490
+ function requireApprovalKind(key) {
491
+ const kind = kinds.get(key);
492
+ if (!kind) throw new ApprovalError("APPROVAL_KIND_UNKNOWN", `Approval kind '${key}' is not defined in this process. Import the package that calls defineApprovalKind() for it.`);
493
+ return kind;
494
+ }
495
+ function unregisterApprovalKind(key) {
496
+ return kinds.delete(key);
497
+ }
498
+ function assertPolicyTightens(kind, rules) {
499
+ const quorum = rules.requiredApprovals ?? 0;
500
+ if (quorum !== 0) {
501
+ assertQuorum(quorum, `Approval policy for '${kind.key}' requiredApprovals`);
502
+ if (quorum < kind.defaults.requiredApprovals) throw new ApprovalError("APPROVAL_POLICY_LOOSENS", `Approval policy for '${kind.key}' may only raise requiredApprovals (default ${kind.defaults.requiredApprovals}), got ${quorum}.`);
503
+ }
504
+ const ttl = rules.ttlMs ?? 0;
505
+ if (ttl !== 0) {
506
+ assertTtl(ttl, `Approval policy for '${kind.key}' ttlMs`);
507
+ if (ttl > kind.defaults.ttlMs) throw new ApprovalError("APPROVAL_POLICY_LOOSENS", `Approval policy for '${kind.key}' may only shorten ttlMs (default ${kind.defaults.ttlMs}), got ${ttl}.`);
508
+ }
509
+ const permission = rules.requiredPermission ?? "";
510
+ if (permission !== "" && !isPermissionSlug(permission)) throw new ApprovalError("APPROVAL_INVALID", `Approval policy for '${kind.key}' requiredPermission must be a permission slug, got '${permission}'.`);
511
+ }
512
+ function effectiveRules(kind, policy) {
513
+ const policyQuorum = Number(policy?.requiredApprovals ?? 0);
514
+ const policyTtl = Number(policy?.ttlMs ?? 0);
515
+ const requiredApprovals = Number.isSafeInteger(policyQuorum) && policyQuorum > 0 ? Math.min(Math.max(kind.defaults.requiredApprovals, policyQuorum), 25) : kind.defaults.requiredApprovals;
516
+ const ttlMs = Number.isSafeInteger(policyTtl) && policyTtl > 0 ? Math.max(Math.min(kind.defaults.ttlMs, policyTtl), MIN_APPROVAL_TTL_MS) : kind.defaults.ttlMs;
517
+ const requiredPermissions = [kind.permission];
518
+ const extra = String(policy?.requiredPermission ?? "").trim();
519
+ if (extra && !requiredPermissions.includes(extra)) requiredPermissions.push(extra);
520
+ return {
521
+ requiredApprovals,
522
+ ttlMs,
523
+ requiredPermissions
524
+ };
525
+ }
526
+ //#endregion
527
+ //#region src/models/ApprovalPolicy.ts
528
+ var __defProp$2 = Object.defineProperty;
529
+ var __getOwnPropDesc$2 = Object.getOwnPropertyDescriptor;
530
+ var __defNormalProp$2 = (obj, key, value) => key in obj ? __defProp$2(obj, key, {
531
+ enumerable: true,
532
+ configurable: true,
533
+ writable: true,
534
+ value
535
+ }) : obj[key] = value;
536
+ var __decorateClass$2 = (decorators, target, key, kind) => {
537
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$2(target, key) : target;
538
+ for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
539
+ if (kind && result) __defProp$2(target, key, result);
540
+ return result;
541
+ };
542
+ var __publicField$2 = (obj, key, value) => __defNormalProp$2(obj, typeof key !== "symbol" ? key + "" : key, value);
543
+ var ApprovalPolicy = class extends SmrtObject {
544
+ tenantId = "";
545
+ kind = "";
546
+ requiredApprovals = 0;
547
+ ttlMs = 0;
548
+ requiredPermission = "";
549
+ updatedBy = "";
550
+ constructor(options = {}) {
551
+ super(options);
552
+ if (options.tenantId !== void 0) this.tenantId = options.tenantId;
553
+ if (options.kind !== void 0) this.kind = options.kind;
554
+ if (options.requiredApprovals !== void 0) this.requiredApprovals = options.requiredApprovals;
555
+ if (options.ttlMs !== void 0) this.ttlMs = options.ttlMs;
556
+ if (options.requiredPermission !== void 0) this.requiredPermission = options.requiredPermission;
557
+ if (options.updatedBy !== void 0) this.updatedBy = options.updatedBy;
558
+ }
559
+ /**
560
+ * Validate against the registered kind, then persist. A row for an unknown
561
+ * kind, or one that would loosen the kind, is refused.
562
+ *
563
+ * @throws {ApprovalError} `APPROVAL_KIND_UNKNOWN`,
564
+ * `APPROVAL_POLICY_LOOSENS`, or `APPROVAL_INVALID`.
565
+ */
566
+ async save() {
567
+ this.requiredPermission = String(this.requiredPermission ?? "").trim();
568
+ this.requiredApprovals = Number(this.requiredApprovals ?? 0);
569
+ this.ttlMs = Number(this.ttlMs ?? 0);
570
+ assertPolicyTightens(requireApprovalKind(this.kind), {
571
+ requiredApprovals: this.requiredApprovals,
572
+ ttlMs: this.ttlMs,
573
+ requiredPermission: this.requiredPermission
574
+ });
575
+ return await super.save();
576
+ }
577
+ };
578
+ __publicField$2(ApprovalPolicy, "__smrtPackage__", "@happyvertical/smrt-approvals");
579
+ __decorateClass$2([tenantId()], ApprovalPolicy.prototype, "tenantId", 2);
580
+ __decorateClass$2([field({
581
+ type: "text",
582
+ required: true
583
+ })], ApprovalPolicy.prototype, "kind", 2);
584
+ __decorateClass$2([field({ type: "integer" })], ApprovalPolicy.prototype, "requiredApprovals", 2);
585
+ __decorateClass$2([field({ type: "integer" })], ApprovalPolicy.prototype, "ttlMs", 2);
586
+ __decorateClass$2([field({ type: "text" })], ApprovalPolicy.prototype, "requiredPermission", 2);
587
+ __decorateClass$2([field({ type: "text" })], ApprovalPolicy.prototype, "updatedBy", 2);
588
+ ApprovalPolicy = __decorateClass$2([TenantScoped({ mode: "required" }), smrt({
589
+ tableName: "approval_policies",
590
+ api: { include: ["list", "get"] },
591
+ mcp: { include: ["list", "get"] },
592
+ cli: false,
593
+ indexes: [{
594
+ name: "approval_policies_tenant_kind_key",
595
+ columns: ["tenantId", "kind"],
596
+ unique: true
597
+ }]
598
+ })], ApprovalPolicy);
599
+ //#endregion
600
+ //#region src/collections/ApprovalPolicyCollection.ts
601
+ var __defProp$1 = Object.defineProperty;
602
+ var __getOwnPropDesc$1 = Object.getOwnPropertyDescriptor;
603
+ var __defNormalProp$1 = (obj, key, value) => key in obj ? __defProp$1(obj, key, {
604
+ enumerable: true,
605
+ configurable: true,
606
+ writable: true,
607
+ value
608
+ }) : obj[key] = value;
609
+ var __decorateClass$1 = (decorators, target, key, kind) => {
610
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$1(target, key) : target;
611
+ for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
612
+ if (kind && result) __defProp$1(target, key, result);
613
+ return result;
614
+ };
615
+ var __publicField$1 = (obj, key, value) => __defNormalProp$1(obj, typeof key !== "symbol" ? key + "" : key, value);
616
+ var ApprovalPolicyCollection = class extends SmrtCollection {};
617
+ __publicField$1(ApprovalPolicyCollection, "__smrtPackage__", "@happyvertical/smrt-approvals");
618
+ __publicField$1(ApprovalPolicyCollection, "_itemClass", ApprovalPolicy);
619
+ ApprovalPolicyCollection = __decorateClass$1([smrt({
620
+ api: { include: ["list", "get"] },
621
+ mcp: { include: ["list", "get"] },
622
+ cli: false
623
+ })], ApprovalPolicyCollection);
624
+ //#endregion
625
+ //#region src/collections/ApprovalRequestCollection.ts
626
+ var __defProp = Object.defineProperty;
627
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
628
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, {
629
+ enumerable: true,
630
+ configurable: true,
631
+ writable: true,
632
+ value
633
+ }) : obj[key] = value;
634
+ var __decorateClass = (decorators, target, key, kind) => {
635
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
636
+ for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
637
+ if (kind && result) __defProp(target, key, result);
638
+ return result;
639
+ };
640
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
641
+ var ApprovalRequestCollection = class extends SmrtCollection {};
642
+ __publicField(ApprovalRequestCollection, "__smrtPackage__", "@happyvertical/smrt-approvals");
643
+ __publicField(ApprovalRequestCollection, "_itemClass", ApprovalRequest);
644
+ ApprovalRequestCollection = __decorateClass([smrt({
645
+ api: { include: ["list", "get"] },
646
+ mcp: { include: ["list", "get"] },
647
+ cli: false
648
+ })], ApprovalRequestCollection);
649
+ //#endregion
650
+ //#region src/service.ts
651
+ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
652
+ var MAX_TOKEN_LENGTH = 256;
653
+ var MAX_TEXT_LENGTH = 4e3;
654
+ var DEFAULT_MAX_ATTEMPTS = 8;
655
+ var REQUESTS_TABLE = "approval_requests";
656
+ var DECISION_EVENT = {
657
+ approve: "approved",
658
+ reject: "rejected",
659
+ request_changes: "changes_requested"
660
+ };
661
+ function refusal(reason, detail, id = null) {
662
+ return {
663
+ kind: "refused",
664
+ reason,
665
+ detail,
666
+ id
667
+ };
668
+ }
669
+ function isoNow(date) {
670
+ return date.toISOString();
671
+ }
672
+ function assertToken(value, label) {
673
+ const text = typeof value === "string" ? value.trim() : "";
674
+ if (!text || text.length > MAX_TOKEN_LENGTH) throw new ApprovalError("APPROVAL_INVALID", `${label} must be a non-empty string of at most ${MAX_TOKEN_LENGTH} characters.`);
675
+ return text;
676
+ }
677
+ function assertText(value, label) {
678
+ if (value === void 0 || value === null) return "";
679
+ if (typeof value !== "string" || value.length > MAX_TEXT_LENGTH) throw new ApprovalError("APPROVAL_INVALID", `${label} must be a string of at most ${MAX_TEXT_LENGTH} characters.`);
680
+ return value.trim();
681
+ }
682
+ function assertPrincipal(principal) {
683
+ if (!principal || typeof principal.can !== "function") throw new ApprovalError("APPROVAL_INVALID", "An approval principal must provide can(slug).");
684
+ assertToken(principal.id, "Approval principal id");
685
+ if (typeof principal.tenantId !== "string" || !UUID_RE.test(principal.tenantId)) throw new ApprovalError("APPROVAL_INVALID", "An approval principal must carry the UUID of the tenant its permissions were resolved for.");
686
+ if (!APPROVAL_PRINCIPAL_TYPES.includes(principal.type)) throw new ApprovalError("APPROVAL_INVALID", `Approval principal type must be one of ${APPROVAL_PRINCIPAL_TYPES.join(", ")}.`);
687
+ }
688
+ function countedApprovers(approvals, request) {
689
+ return new Set(approvals.filter((event) => event.type === "approved" && event.actorType === "human" && event.actorId !== request.requestedBy).map((event) => event.actorId));
690
+ }
691
+ function rowVersion(rows) {
692
+ const row = rows?.[0];
693
+ if (!row) return null;
694
+ const version = Number(row.version);
695
+ return Number.isSafeInteger(version) ? version : null;
696
+ }
697
+ var ApprovalService = class ApprovalService {
698
+ db;
699
+ clock;
700
+ maxAttempts;
701
+ constructor(options) {
702
+ if (!options?.db || typeof options.db.query !== "function") throw new ApprovalError("APPROVAL_INVALID", "ApprovalService requires an initialized database.");
703
+ if (typeof options.db.transaction !== "function") throw new ApprovalError("APPROVAL_NO_TRANSACTION", "ApprovalService requires a database adapter with transaction support.");
704
+ this.db = options.db;
705
+ this.clock = options.now ?? (() => /* @__PURE__ */ new Date());
706
+ this.maxAttempts = Math.max(1, options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS);
707
+ }
708
+ /** One request in the principal's tenant, or `null`. */
709
+ async getRequest(principal, requestId) {
710
+ assertPrincipal(principal);
711
+ if (!UUID_RE.test(String(requestId ?? ""))) return null;
712
+ return await this.inTenant(principal, () => this.loadRequest(this.db, principal.tenantId, requestId));
713
+ }
714
+ /** A request's events in order, in the principal's tenant. */
715
+ async listEvents(principal, requestId) {
716
+ assertPrincipal(principal);
717
+ if (!UUID_RE.test(String(requestId ?? ""))) return [];
718
+ return await this.inTenant(principal, async () => {
719
+ return await (await ApprovalEventCollection.create({ db: this.db })).list({
720
+ where: {
721
+ tenantId: principal.tenantId,
722
+ requestId
723
+ },
724
+ orderBy: "sequence ASC",
725
+ limit: 1e3
726
+ });
727
+ });
728
+ }
729
+ /**
730
+ * Open a request for approval of one subject revision. Any principal type
731
+ * may request. The quorum, expiry, and required permissions are fixed now
732
+ * from the kind's defaults tightened by the tenant's policy.
733
+ */
734
+ async requestApproval(principal, input) {
735
+ assertPrincipal(principal);
736
+ const kind = requireApprovalKind(typeof input.kind === "string" ? input.kind : input.kind?.key);
737
+ const subjectId = assertToken(input.subjectId, "subjectId");
738
+ const subjectRevisionHash = assertToken(input.subjectRevisionHash, "subjectRevisionHash");
739
+ const requestKey = input.requestKey === void 0 ? globalThis.crypto.randomUUID() : assertToken(input.requestKey, "requestKey");
740
+ const summary = assertText(input.summary, "summary");
741
+ const tenantId = principal.tenantId;
742
+ const sameContent = (row) => row.kind === kind.key && row.subjectType === kind.subject && row.subjectId === subjectId && row.subjectRevisionHash === subjectRevisionHash;
743
+ if (this.contextMismatch(principal)) return ApprovalService.mismatchResult();
744
+ return await this.inTenant(principal, async () => {
745
+ let outcome;
746
+ try {
747
+ outcome = await this.runTransaction(async (tx) => {
748
+ const requests = await this.txCollection(ApprovalRequestCollection, tx);
749
+ const [existing] = await requests.list({
750
+ where: {
751
+ tenantId,
752
+ requestKey
753
+ },
754
+ limit: 1
755
+ });
756
+ if (existing) return sameContent(existing) ? {
757
+ kind: "done",
758
+ outcome: "already_applied",
759
+ id: String(existing.id)
760
+ } : refusal("request_key_conflict", `requestKey '${requestKey}' already names a different request`);
761
+ const policy = await this.loadPolicy(tx, tenantId, kind.key);
762
+ const rules = effectiveRules(kind, policy);
763
+ const now = this.clock();
764
+ const request = await requests.create({
765
+ tenantId,
766
+ kind: kind.key,
767
+ subjectType: kind.subject,
768
+ subjectId,
769
+ subjectRevisionHash,
770
+ requestKey,
771
+ requestedBy: principal.id,
772
+ requestedByType: principal.type,
773
+ summary,
774
+ status: "pending",
775
+ requiredApprovals: rules.requiredApprovals,
776
+ approvalCount: 0,
777
+ requiredPermissions: JSON.stringify(rules.requiredPermissions),
778
+ version: 1,
779
+ expiresAt: new Date(now.getTime() + rules.ttlMs),
780
+ ...serviceWriteOption()
781
+ });
782
+ const id = String(request.id);
783
+ await this.appendEvent(tx, {
784
+ tenantId,
785
+ requestId: id,
786
+ type: "created",
787
+ principal,
788
+ reason: summary,
789
+ subjectRevisionHash,
790
+ sequence: 1,
791
+ now
792
+ });
793
+ return {
794
+ kind: "done",
795
+ outcome: "transitioned",
796
+ id
797
+ };
798
+ });
799
+ } catch (error) {
800
+ if (!isUniqueViolationError(error)) throw error;
801
+ const [winner] = await (await ApprovalRequestCollection.create({ db: this.db })).list({
802
+ where: {
803
+ tenantId,
804
+ requestKey
805
+ },
806
+ limit: 1
807
+ });
808
+ if (!winner) throw error;
809
+ outcome = sameContent(winner) ? {
810
+ kind: "done",
811
+ outcome: "already_applied",
812
+ id: String(winner.id)
813
+ } : refusal("request_key_conflict", `requestKey '${requestKey}' already names a different request`);
814
+ }
815
+ return await this.toResult(principal.tenantId, outcome);
816
+ });
817
+ }
818
+ /**
819
+ * Record a decision. Only a `human` principal of the request's tenant who
820
+ * is not the requester and holds every required permission may decide,
821
+ * once, while the request is pending and unexpired. `approve` counts
822
+ * toward the quorum of distinct approvers; `reject` and `request_changes`
823
+ * end the request.
824
+ */
825
+ async decide(principal, requestId, input) {
826
+ assertPrincipal(principal);
827
+ const eventType = DECISION_EVENT[input?.decision];
828
+ if (!eventType) throw new ApprovalError("APPROVAL_INVALID", `Unknown decision '${String(input?.decision)}'.`);
829
+ const reason = assertText(input.reason, "reason");
830
+ if (input.decision !== "approve" && !reason) throw new ApprovalError("APPROVAL_INVALID", `A '${input.decision}' decision requires a reason.`);
831
+ const tenantId = principal.tenantId;
832
+ return await this.transition(principal, requestId, async (tx, request, now) => {
833
+ if (principal.type !== "human") return refusal("not_human", `a ${principal.type} principal cannot decide`, request.id);
834
+ if (principal.id === request.requestedBy) return refusal("self_approval", "the requester cannot decide their own request", request.id);
835
+ const missing = (await this.requiredPermissionsFor(tx, request)).filter((slug) => !principal.can(slug));
836
+ if (missing.length > 0) return refusal("forbidden", `missing permission: ${missing.join(", ")}`, request.id);
837
+ if (request.status !== "pending") return refusal("not_pending", `request is ${request.status}`, request.id);
838
+ if (!(request.expiresAtMs() > now.getTime())) return refusal("expired", "the request expired", request.id);
839
+ const events = await this.txCollection(ApprovalEventCollection, tx);
840
+ if ((await events.list({
841
+ where: {
842
+ tenantId,
843
+ requestId: request.id,
844
+ voteKey: principal.id
845
+ },
846
+ limit: 1
847
+ })).length > 0) return refusal("already_decided", "this principal already decided", request.id);
848
+ const version = Number(request.version);
849
+ const nowIso = isoNow(now);
850
+ let updated;
851
+ if (input.decision === "approve") {
852
+ const voters = countedApprovers(await events.list({
853
+ where: {
854
+ tenantId,
855
+ requestId: request.id,
856
+ type: "approved"
857
+ },
858
+ limit: 1e3
859
+ }), request);
860
+ voters.add(principal.id);
861
+ const count = voters.size;
862
+ const reached = count >= Number(request.requiredApprovals);
863
+ updated = await tx.query(`UPDATE ${REQUESTS_TABLE}
864
+ SET approval_count = ?, status = ?, decided_at = ${reached ? "?" : "decided_at"},
865
+ version = version + 1, updated_at = ?
866
+ WHERE id = ? AND tenant_id = ? AND status = 'pending'
867
+ AND version = ? AND expires_at > ?
868
+ RETURNING version`, ...[
869
+ count,
870
+ reached ? "approved" : "pending",
871
+ ...reached ? [nowIso] : [],
872
+ nowIso,
873
+ request.id,
874
+ tenantId,
875
+ version,
876
+ nowIso
877
+ ]);
878
+ } else {
879
+ const status = input.decision === "reject" ? "rejected" : "changes_requested";
880
+ updated = await tx.query(`UPDATE ${REQUESTS_TABLE}
881
+ SET status = ?, decided_at = ?, version = version + 1, updated_at = ?
882
+ WHERE id = ? AND tenant_id = ? AND status = 'pending'
883
+ AND version = ? AND expires_at > ?
884
+ RETURNING version`, status, nowIso, nowIso, request.id, tenantId, version, nowIso);
885
+ }
886
+ const next = rowVersion(updated.rows);
887
+ if (next === null) return { kind: "retry" };
888
+ await this.appendEvent(tx, {
889
+ tenantId,
890
+ requestId: String(request.id),
891
+ type: eventType,
892
+ principal,
893
+ reason,
894
+ subjectRevisionHash: request.subjectRevisionHash,
895
+ sequence: next,
896
+ voteKey: principal.id,
897
+ now
898
+ });
899
+ await this.recordChange(tx, String(request.id), tenantId);
900
+ return {
901
+ kind: "done",
902
+ outcome: "transitioned",
903
+ id: String(request.id)
904
+ };
905
+ });
906
+ }
907
+ /**
908
+ * Cancel a pending request. The requester may cancel their own request;
909
+ * anyone else needs `approvals.cancel-any`.
910
+ */
911
+ async cancel(principal, requestId, input = {}) {
912
+ assertPrincipal(principal);
913
+ const reason = assertText(input.reason, "reason");
914
+ const tenantId = principal.tenantId;
915
+ return await this.transition(principal, requestId, async (tx, request, now) => {
916
+ if (principal.id !== request.requestedBy && !principal.can("approvals.cancel-any")) return refusal("forbidden", `only the requester or a holder of ${CANCEL_ANY_APPROVAL_PERMISSION} may cancel`, request.id);
917
+ if (request.status !== "pending") return refusal("not_pending", `request is ${request.status}`, request.id);
918
+ if (!(request.expiresAtMs() > now.getTime())) return refusal("expired", "the request expired", request.id);
919
+ const nowIso = isoNow(now);
920
+ const next = rowVersion((await tx.query(`UPDATE ${REQUESTS_TABLE}
921
+ SET status = 'cancelled', decided_at = ?, version = version + 1, updated_at = ?
922
+ WHERE id = ? AND tenant_id = ? AND status = 'pending'
923
+ AND version = ? AND expires_at > ?
924
+ RETURNING version`, nowIso, nowIso, request.id, tenantId, Number(request.version), nowIso)).rows);
925
+ if (next === null) return { kind: "retry" };
926
+ await this.appendEvent(tx, {
927
+ tenantId,
928
+ requestId: String(request.id),
929
+ type: "cancelled",
930
+ principal,
931
+ reason,
932
+ subjectRevisionHash: request.subjectRevisionHash,
933
+ sequence: next,
934
+ now
935
+ });
936
+ await this.recordChange(tx, String(request.id), tenantId);
937
+ return {
938
+ kind: "done",
939
+ outcome: "transitioned",
940
+ id: String(request.id)
941
+ };
942
+ });
943
+ }
944
+ /**
945
+ * Mark one request expired: a pending request, or an approved one that
946
+ * was never consumed, whose expiry has passed. Needs no permission (it
947
+ * only records what time already decided), so a job may call it.
948
+ */
949
+ async expire(principal, requestId) {
950
+ assertPrincipal(principal);
951
+ const tenantId = principal.tenantId;
952
+ return await this.transition(principal, requestId, async (tx, request, now) => {
953
+ if (!(request.status === "pending" || request.status === "approved" && !request.isConsumed())) return refusal("not_pending", request.status === "approved" ? "the approval was consumed" : `request is ${request.status}`, request.id);
954
+ if (request.expiresAtMs() > now.getTime()) return refusal("not_expired", "the request has not expired", request.id);
955
+ const nowIso = isoNow(now);
956
+ const next = rowVersion((await tx.query(`UPDATE ${REQUESTS_TABLE}
957
+ SET status = 'expired', decided_at = ?, version = version + 1, updated_at = ?
958
+ WHERE id = ? AND tenant_id = ? AND status IN ('pending', 'approved')
959
+ AND consumed_at IS NULL AND version = ? AND expires_at <= ?
960
+ RETURNING version`, nowIso, nowIso, request.id, tenantId, Number(request.version), nowIso)).rows);
961
+ if (next === null) return { kind: "retry" };
962
+ await this.appendEvent(tx, {
963
+ tenantId,
964
+ requestId: String(request.id),
965
+ type: "expired",
966
+ principal,
967
+ reason: "",
968
+ subjectRevisionHash: request.subjectRevisionHash,
969
+ sequence: next,
970
+ now
971
+ });
972
+ await this.recordChange(tx, String(request.id), tenantId);
973
+ return {
974
+ kind: "done",
975
+ outcome: "transitioned",
976
+ id: String(request.id)
977
+ };
978
+ });
979
+ }
980
+ /**
981
+ * Expire every due request in the principal's tenant: pending requests
982
+ * and unconsumed approvals past their deadline, oldest expiry first, at
983
+ * most `limit`. Returns the ids this call expired.
984
+ */
985
+ async expireDue(principal, options = {}) {
986
+ assertPrincipal(principal);
987
+ const limit = Math.min(Math.max(1, Math.trunc(options.limit ?? 100)), 1e3);
988
+ const now = this.clock();
989
+ const candidates = await this.inTenant(principal, async () => {
990
+ return await (await ApprovalRequestCollection.create({ db: this.db })).list({
991
+ where: {
992
+ tenantId: principal.tenantId,
993
+ status: ["pending", "approved"],
994
+ consumedAt: null,
995
+ "expiresAt <=": now
996
+ },
997
+ orderBy: "expires_at ASC",
998
+ limit
999
+ });
1000
+ });
1001
+ const expired = [];
1002
+ for (const candidate of candidates) if ((await this.expire(principal, String(candidate.id))).outcome === "transitioned") expired.push(String(candidate.id));
1003
+ return expired;
1004
+ }
1005
+ /**
1006
+ * Consume an approval exactly once. The presented hash must equal the
1007
+ * revision that was approved; the request must be approved, unconsumed,
1008
+ * and unexpired. Any principal type of the tenant may consume (the
1009
+ * consumer's own service is the executor). Pass `options.transaction` to
1010
+ * make the consumption part of the consumer's own transaction.
1011
+ */
1012
+ async consume(principal, requestId, subjectRevisionHash, options = {}) {
1013
+ assertPrincipal(principal);
1014
+ const hash = assertToken(subjectRevisionHash, "subjectRevisionHash");
1015
+ const tenantId = principal.tenantId;
1016
+ return await this.transition(principal, requestId, async (tx, request, now) => {
1017
+ if (request.status !== "approved") return refusal("not_approved", `request is ${request.status}`, request.id);
1018
+ if (request.isConsumed()) return refusal("already_consumed", "the approval was already used", request.id);
1019
+ if (request.subjectRevisionHash !== hash) return refusal("revision_mismatch", "the subject changed after it was approved", request.id);
1020
+ if (!(request.expiresAtMs() > now.getTime())) return refusal("expired", "the approval expired", request.id);
1021
+ if (!await this.ledgerBacksApproval(tx, request)) return refusal("unbacked_approval", "the ledger does not record this approval", request.id);
1022
+ const nowIso = isoNow(now);
1023
+ const next = rowVersion((await tx.query(`UPDATE ${REQUESTS_TABLE}
1024
+ SET consumed_at = ?, consumed_by = ?, version = version + 1, updated_at = ?
1025
+ WHERE id = ? AND tenant_id = ? AND status = 'approved'
1026
+ AND consumed_at IS NULL AND subject_revision_hash = ?
1027
+ AND version = ? AND expires_at > ?
1028
+ RETURNING version`, nowIso, principal.id, nowIso, request.id, tenantId, hash, Number(request.version), nowIso)).rows);
1029
+ if (next === null) return { kind: "retry" };
1030
+ await this.appendEvent(tx, {
1031
+ tenantId,
1032
+ requestId: String(request.id),
1033
+ type: "consumed",
1034
+ principal,
1035
+ reason: "",
1036
+ subjectRevisionHash: hash,
1037
+ sequence: next,
1038
+ now
1039
+ });
1040
+ await this.recordChange(tx, String(request.id), tenantId);
1041
+ return {
1042
+ kind: "done",
1043
+ outcome: "transitioned",
1044
+ id: String(request.id)
1045
+ };
1046
+ }, options.transaction);
1047
+ }
1048
+ /**
1049
+ * Write the tenant's tightening of a kind. Needs
1050
+ * `approvals.manage-policy`; a rule that would loosen the kind's defaults
1051
+ * throws `APPROVAL_POLICY_LOOSENS`. `0`/`''` clear a rule back to the
1052
+ * default. Applies to requests made afterwards.
1053
+ */
1054
+ async setPolicy(principal, kindKey, rules) {
1055
+ assertPrincipal(principal);
1056
+ if (principal.type !== "human" || !principal.can("approvals.manage-policy")) throw new ApprovalError("APPROVAL_FORBIDDEN", `Setting approval policy needs a human principal holding ${MANAGE_APPROVAL_POLICY_PERMISSION}.`);
1057
+ const kind = requireApprovalKind(typeof kindKey === "string" ? kindKey : kindKey?.key);
1058
+ const normalized = {
1059
+ requiredApprovals: rules.requiredApprovals ?? 0,
1060
+ ttlMs: rules.ttlMs ?? 0,
1061
+ requiredPermission: String(rules.requiredPermission ?? "").trim()
1062
+ };
1063
+ if (normalized.requiredPermission && !isPermissionSlug(normalized.requiredPermission)) throw new ApprovalError("APPROVAL_INVALID", `requiredPermission must be a permission slug, got '${normalized.requiredPermission}'.`);
1064
+ assertPolicyTightens(kind, normalized);
1065
+ const tenantId = principal.tenantId;
1066
+ return await this.inTenant(principal, async () => {
1067
+ const id = await this.runTransaction(async (tx) => {
1068
+ const policies2 = await this.txCollection(ApprovalPolicyCollection, tx);
1069
+ const [existing] = await policies2.list({
1070
+ where: {
1071
+ tenantId,
1072
+ kind: kind.key
1073
+ },
1074
+ limit: 1
1075
+ });
1076
+ if (existing) {
1077
+ existing.requiredApprovals = normalized.requiredApprovals;
1078
+ existing.ttlMs = normalized.ttlMs;
1079
+ existing.requiredPermission = normalized.requiredPermission;
1080
+ existing.updatedBy = principal.id;
1081
+ await existing.save();
1082
+ return String(existing.id);
1083
+ }
1084
+ const created = await policies2.create({
1085
+ tenantId,
1086
+ kind: kind.key,
1087
+ ...normalized,
1088
+ updatedBy: principal.id
1089
+ });
1090
+ return String(created.id);
1091
+ });
1092
+ const [saved] = await (await ApprovalPolicyCollection.create({ db: this.db })).list({
1093
+ where: {
1094
+ tenantId,
1095
+ id
1096
+ },
1097
+ limit: 1
1098
+ });
1099
+ if (!saved) throw new ApprovalError("APPROVAL_INVALID", "Saved approval policy vanished.");
1100
+ return saved;
1101
+ });
1102
+ }
1103
+ /** The effective rules a new request of `kind` would get in the tenant. */
1104
+ async rulesFor(principal, kindKey) {
1105
+ assertPrincipal(principal);
1106
+ const kind = requireApprovalKind(typeof kindKey === "string" ? kindKey : kindKey?.key);
1107
+ return await this.inTenant(principal, async () => effectiveRules(kind, await this.loadPolicy(this.db, principal.tenantId, kind.key)));
1108
+ }
1109
+ /**
1110
+ * Run a guarded transition with retry. `body` runs inside a transaction
1111
+ * on a fresh read of the request and returns `retry` when its guarded
1112
+ * UPDATE matched no row (a concurrent transition won).
1113
+ */
1114
+ async transition(principal, requestId, body, callerTransaction) {
1115
+ if (!UUID_RE.test(String(requestId ?? ""))) return {
1116
+ outcome: "refused",
1117
+ request: null,
1118
+ refusal: {
1119
+ reason: "not_found",
1120
+ detail: "not a request id"
1121
+ }
1122
+ };
1123
+ if (this.contextMismatch(principal)) return ApprovalService.mismatchResult();
1124
+ const tenantId = principal.tenantId;
1125
+ return await this.inTenant(principal, async () => {
1126
+ const attempt = async (tx) => {
1127
+ const request = await this.loadRequest(tx, tenantId, requestId);
1128
+ if (!request) return refusal("not_found", "no such request in this tenant");
1129
+ return await body(tx, request, this.clock());
1130
+ };
1131
+ if (callerTransaction) {
1132
+ const outcome = await attempt(callerTransaction);
1133
+ if (outcome.kind === "retry") return await this.toResult(tenantId, refusal("contention", "a concurrent transition won", requestId), callerTransaction);
1134
+ return await this.toResult(tenantId, outcome, callerTransaction);
1135
+ }
1136
+ for (let i = 0; i < this.maxAttempts; i++) {
1137
+ let outcome;
1138
+ try {
1139
+ outcome = await this.runTransaction(attempt);
1140
+ } catch (error) {
1141
+ if (isUniqueViolationError(error)) continue;
1142
+ throw error;
1143
+ }
1144
+ if (outcome.kind !== "retry") return await this.toResult(tenantId, outcome);
1145
+ }
1146
+ return await this.toResult(tenantId, refusal("contention", "concurrent transitions kept winning", requestId));
1147
+ });
1148
+ }
1149
+ /** True when an active tenant context names a different tenant. */
1150
+ contextMismatch(principal) {
1151
+ const active = getTenantId();
1152
+ return !isSystemContext() && active !== void 0 && active !== principal.tenantId;
1153
+ }
1154
+ /** The refusal returned when {@link contextMismatch} holds. */
1155
+ static mismatchResult() {
1156
+ return {
1157
+ outcome: "refused",
1158
+ request: null,
1159
+ refusal: {
1160
+ reason: "tenant_mismatch",
1161
+ detail: "the principal's tenant does not match the active tenant context"
1162
+ }
1163
+ };
1164
+ }
1165
+ /** Pin the work to the principal's tenant; refuse a conflicting context. */
1166
+ async inTenant(principal, fn) {
1167
+ if (this.contextMismatch(principal)) throw new ApprovalError("APPROVAL_FORBIDDEN", `The principal's tenant does not match the active tenant context.`);
1168
+ return await withTenant({ tenantId: principal.tenantId }, fn);
1169
+ }
1170
+ /**
1171
+ * Run `fn` in a transaction through core's embedded write queue.
1172
+ *
1173
+ * On embedded engines (SQLite, DuckDB, libsql) the transaction takes the
1174
+ * queue for its database BEFORE the adapter's connection lock, and the
1175
+ * model writes inside it (event inserts, change-feed appends) re-enter that
1176
+ * hold. Taking the connection lock first deadlocks against an unrelated
1177
+ * save that holds the queue and then opens its own write transaction, and
1178
+ * lets other root writes land inside this BEGIN..COMMIT. The queue also
1179
+ * serializes these transactions per database in-process, which is what
1180
+ * the former per-handle promise chain did, so that chain is gone.
1181
+ * PostgreSQL is not queued: each transaction has its own pooled
1182
+ * connection and the guarded UPDATE's row lock arbitrates.
1183
+ */
1184
+ async runTransaction(fn) {
1185
+ return await withEmbeddedWriteTransaction(this.db, isEmbeddedDatabase(this.db), fn);
1186
+ }
1187
+ /** A collection bound to a transaction database. */
1188
+ async txCollection(Collection, tx) {
1189
+ return await Collection.create({
1190
+ db: tx,
1191
+ _reuseInitializedDb: true,
1192
+ _deferRuntimeInitialization: true
1193
+ });
1194
+ }
1195
+ async loadRequest(db, tenantId, requestId) {
1196
+ const [request] = await (db === this.db ? await ApprovalRequestCollection.create({ db }) : await this.txCollection(ApprovalRequestCollection, db)).list({
1197
+ where: {
1198
+ tenantId,
1199
+ id: requestId
1200
+ },
1201
+ limit: 1
1202
+ });
1203
+ return request && request.tenantId === tenantId ? request : null;
1204
+ }
1205
+ async loadPolicy(db, tenantId, kind) {
1206
+ const [policy] = await (db === this.db ? await ApprovalPolicyCollection.create({ db }) : await this.txCollection(ApprovalPolicyCollection, db)).list({
1207
+ where: {
1208
+ tenantId,
1209
+ kind
1210
+ },
1211
+ limit: 1
1212
+ });
1213
+ return policy ?? null;
1214
+ }
1215
+ /**
1216
+ * Every slug a decider must hold now: the slugs fixed at request time
1217
+ * plus the kind's and the tenant policy's current ones (tightening only:
1218
+ * a later policy can add a slug, never remove one).
1219
+ */
1220
+ async requiredPermissionsFor(tx, request) {
1221
+ const kind = requireApprovalKind(request.kind);
1222
+ const current = effectiveRules(kind, await this.loadPolicy(tx, request.tenantId, kind.key));
1223
+ const slugs = new Set(request.getRequiredPermissions());
1224
+ for (const slug of current.requiredPermissions) slugs.add(slug);
1225
+ return [...slugs];
1226
+ }
1227
+ async appendEvent(tx, input) {
1228
+ await (await this.txCollection(ApprovalEventCollection, tx)).create({
1229
+ tenantId: input.tenantId,
1230
+ requestId: input.requestId,
1231
+ type: input.type,
1232
+ actorId: input.principal.id,
1233
+ actorType: input.principal.type,
1234
+ reason: input.reason,
1235
+ subjectRevisionHash: input.subjectRevisionHash,
1236
+ sequence: input.sequence,
1237
+ voteKey: input.voteKey ?? null,
1238
+ occurredAt: input.now,
1239
+ ...serviceWriteOption()
1240
+ });
1241
+ }
1242
+ /**
1243
+ * Defence in depth for `consume`: the request row says `approved`, so the
1244
+ * ledger must agree. It needs the `created` event at sequence 1, no
1245
+ * terminal event, and approvals from at least `requiredApprovals` distinct
1246
+ * human actors other than the requester. Catches a status written around
1247
+ * the service (raw SQL); model-layer inserts are already capability-gated.
1248
+ */
1249
+ async ledgerBacksApproval(tx, request) {
1250
+ const ledger = await (await this.txCollection(ApprovalEventCollection, tx)).list({
1251
+ where: {
1252
+ tenantId: request.tenantId,
1253
+ requestId: request.id
1254
+ },
1255
+ limit: 1e3
1256
+ });
1257
+ const created = ledger.some((event) => event.type === "created" && Number(event.sequence) === 1);
1258
+ const terminal = ledger.some((event) => [
1259
+ "rejected",
1260
+ "changes_requested",
1261
+ "cancelled",
1262
+ "expired",
1263
+ "consumed"
1264
+ ].includes(event.type));
1265
+ const approvers = countedApprovers(ledger.filter((event) => event.type === "approved"), request);
1266
+ return created && !terminal && approvers.size >= Math.max(1, Number(request.requiredApprovals));
1267
+ }
1268
+ /**
1269
+ * The guarded UPDATE bypasses save hooks, so record the change-feed entry
1270
+ * the save path would have written. Best effort, like the save path.
1271
+ */
1272
+ async recordChange(tx, requestId, tenantId) {
1273
+ try {
1274
+ await appendChange(tx, {
1275
+ table: REQUESTS_TABLE,
1276
+ rowId: requestId,
1277
+ operation: "update",
1278
+ tenantId
1279
+ });
1280
+ } catch {}
1281
+ }
1282
+ /** Re-read the request after commit (ids only cross the boundary). */
1283
+ async toResult(tenantId, outcome, db = this.db) {
1284
+ if (outcome.kind === "retry") throw new Error("ApprovalService: unresolved retry outcome");
1285
+ const id = outcome.id;
1286
+ const request = id ? await this.loadRequest(db, tenantId, id) : null;
1287
+ if (outcome.kind === "refused") return {
1288
+ outcome: "refused",
1289
+ request: outcome.reason === "not_found" ? null : request,
1290
+ refusal: {
1291
+ reason: outcome.reason,
1292
+ detail: outcome.detail
1293
+ }
1294
+ };
1295
+ return {
1296
+ outcome: outcome.outcome,
1297
+ request
1298
+ };
1299
+ }
1300
+ };
1301
+ //#endregion
1302
+ //#region src/index.ts
1303
+ ensureApprovalPermissionsRegistered();
1304
+ //#endregion
1305
+ export { APPROVAL_EVENT_TYPES, APPROVAL_PERMISSION_DEFINITIONS, APPROVAL_PRINCIPAL_TYPES, APPROVAL_STATUSES, ApprovalError, ApprovalEvent, ApprovalEventCollection, ApprovalPolicy, ApprovalPolicyCollection, ApprovalRequest, ApprovalRequestCollection, ApprovalService, CANCEL_ANY_APPROVAL_PERMISSION, DEFAULT_APPROVAL_TTL_MS, MANAGE_APPROVAL_POLICY_PERMISSION, MAX_APPROVAL_TTL_MS, MAX_REQUIRED_APPROVALS, MIN_APPROVAL_TTL_MS, approvalPrincipalFromPermissions, assertPolicyTightens, defineApprovalKind, effectiveRules, ensureApprovalPermissionsRegistered, getApprovalKind, listApprovalKinds, registerApprovalPermissions, requireApprovalKind, unregisterApprovalKind };
1306
+
1307
+ //# sourceMappingURL=index.js.map