@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
@@ -0,0 +1,82 @@
1
+ /**
2
+ * ApprovalEvent: the append-only ledger of an approval request.
3
+ *
4
+ * Events are the source of truth for who did what: every transition inserts
5
+ * exactly one event in the same transaction as its guarded UPDATE. The
6
+ * generated surface is read-only, `save()` only ever inserts, and `delete()`
7
+ * is refused. `(requestId, sequence)` is unique (one event per request
8
+ * version) and `(requestId, voteKey)` is unique (one decision per actor).
9
+ *
10
+ * @packageDocumentation
11
+ */
12
+ import { SmrtObject, type SmrtObjectOptions } from '@happyvertical/smrt-core';
13
+ import { type ApprovalEventType, type ApprovalPrincipalType } from '../types.js';
14
+ /** Constructor options for {@link ApprovalEvent}. */
15
+ export interface ApprovalEventOptions extends SmrtObjectOptions {
16
+ tenantId?: string;
17
+ requestId?: string;
18
+ type?: ApprovalEventType;
19
+ actorId?: string;
20
+ actorType?: ApprovalPrincipalType;
21
+ reason?: string;
22
+ subjectRevisionHash?: string;
23
+ sequence?: number;
24
+ voteKey?: string | null;
25
+ occurredAt?: Date | null;
26
+ }
27
+ /** One immutable entry in an approval request's history. */
28
+ export declare class ApprovalEvent extends SmrtObject {
29
+ /** Owning tenant; always the request's tenant. */
30
+ tenantId: string;
31
+ /**
32
+ * The request this event belongs to. DuckDB executes an UPDATE of an
33
+ * indexed column on a referenced row as delete plus insert, so a physical
34
+ * FK there would refuse every transition of a request that has events;
35
+ * the constraint is physical on PostgreSQL and SQLite only. Requests are
36
+ * never deleted, so no engine can orphan an event.
37
+ */
38
+ requestId: string;
39
+ /** What happened. */
40
+ type: ApprovalEventType;
41
+ /** Principal id of the actor. */
42
+ actorId: string;
43
+ /** Principal type of the actor. */
44
+ actorType: ApprovalPrincipalType;
45
+ /** Free-text reason (rejection note, cancellation reason, and so on). */
46
+ reason: string;
47
+ /** The subject revision the request was bound to when this happened. */
48
+ subjectRevisionHash: string;
49
+ /** The request `version` this event produced; unique per request. */
50
+ sequence: number;
51
+ /**
52
+ * The deciding actor's id on decision events, `null` otherwise; unique per
53
+ * request so one actor decides at most once.
54
+ */
55
+ voteKey: string | null;
56
+ /** When the event happened. */
57
+ occurredAt: Date | null;
58
+ constructor(options?: ApprovalEventOptions);
59
+ /**
60
+ * Append the event. An event is written once and never updated.
61
+ *
62
+ * @throws {ApprovalError} `APPROVAL_FORBIDDEN` for a persisted event.
63
+ */
64
+ save(): Promise<this>;
65
+ /**
66
+ * Refuse any insert `ApprovalService` did not mint in the same transaction
67
+ * as its guarded UPDATE (see `write-capability.ts`): a forged `approved`
68
+ * event would otherwise count toward quorum. Runs for ordinary saves and
69
+ * bulk creates alike.
70
+ *
71
+ * @throws {ApprovalError} `APPROVAL_FORBIDDEN`.
72
+ */
73
+ protected validateBeforeSave(): Promise<void>;
74
+ private assertInsertAllowed;
75
+ /**
76
+ * Events are append-only.
77
+ *
78
+ * @throws {ApprovalError} always.
79
+ */
80
+ delete(): Promise<void>;
81
+ }
82
+ //# sourceMappingURL=ApprovalEvent.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ApprovalEvent.d.ts","sourceRoot":"","sources":["../../src/models/ApprovalEvent.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAGL,UAAU,EACV,KAAK,iBAAiB,EAEvB,MAAM,0BAA0B,CAAC;AAElC,OAAO,EAEL,KAAK,iBAAiB,EACtB,KAAK,qBAAqB,EAC3B,MAAM,aAAa,CAAC;AAIrB,qDAAqD;AACrD,MAAM,WAAW,oBAAqB,SAAQ,iBAAiB;IAC7D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,iBAAiB,CAAC;IACzB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,qBAAqB,CAAC;IAClC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,UAAU,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;CAC1B;AAED,4DAA4D;AAC5D,qBAuBa,aAAc,SAAQ,UAAU;IAC3C,kDAAkD;IAElD,QAAQ,EAAE,MAAM,CAAM;IAEtB;;;;;;OAMG;IAQH,SAAS,EAAE,MAAM,CAAM;IAEvB,qBAAqB;IAErB,IAAI,EAAE,iBAAiB,CAAa;IAEpC,iCAAiC;IAEjC,OAAO,EAAE,MAAM,CAAM;IAErB,mCAAmC;IAEnC,SAAS,EAAE,qBAAqB,CAAW;IAE3C,yEAAyE;IAEzE,MAAM,EAAE,MAAM,CAAM;IAEpB,wEAAwE;IAExE,mBAAmB,EAAE,MAAM,CAAM;IAEjC,qEAAqE;IAErE,QAAQ,EAAE,MAAM,CAAK;IAErB;;;OAGG;IAEH,OAAO,EAAE,MAAM,GAAG,IAAI,CAAQ;IAE9B,+BAA+B;IAE/B,UAAU,EAAE,IAAI,GAAG,IAAI,CAAQ;gBAEnB,OAAO,GAAE,oBAAyB;IAgB9C;;;;OAIG;IACY,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAapC;;;;;;;OAOG;cACsB,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC;IAK5D,OAAO,CAAC,mBAAmB;IAQ3B;;;;OAIG;IACY,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;CAMvC"}
@@ -0,0 +1,47 @@
1
+ /**
2
+ * ApprovalPolicy: a tenant's tightening of one approval kind.
3
+ *
4
+ * A row may raise the quorum, shorten the expiry, or add one required
5
+ * permission; it can never loosen the kind's package defaults. Writes go
6
+ * through `ApprovalService.setPolicy`, which refuses loosening, and
7
+ * `effectiveRules` clamps any stored row when a request is made. A policy
8
+ * applies to requests made after it is written; existing requests keep the
9
+ * rules fixed at request time.
10
+ *
11
+ * @packageDocumentation
12
+ */
13
+ import { SmrtObject, type SmrtObjectOptions } from '@happyvertical/smrt-core';
14
+ /** Constructor options for {@link ApprovalPolicy}. */
15
+ export interface ApprovalPolicyOptions extends SmrtObjectOptions {
16
+ tenantId?: string;
17
+ kind?: string;
18
+ requiredApprovals?: number;
19
+ ttlMs?: number;
20
+ requiredPermission?: string;
21
+ updatedBy?: string;
22
+ }
23
+ /** Tenant-level tightening of an approval kind. One row per tenant and kind. */
24
+ export declare class ApprovalPolicy extends SmrtObject {
25
+ /** Owning tenant. */
26
+ tenantId: string;
27
+ /** Approval kind key this row tightens. */
28
+ kind: string;
29
+ /** Raised quorum; `0` inherits the kind default. */
30
+ requiredApprovals: number;
31
+ /** Shortened lifetime in milliseconds; `0` inherits the kind default. */
32
+ ttlMs: number;
33
+ /** Extra permission slug a decider must hold; empty for none. */
34
+ requiredPermission: string;
35
+ /** Principal id that last wrote the row. */
36
+ updatedBy: string;
37
+ constructor(options?: ApprovalPolicyOptions);
38
+ /**
39
+ * Validate against the registered kind, then persist. A row for an unknown
40
+ * kind, or one that would loosen the kind, is refused.
41
+ *
42
+ * @throws {ApprovalError} `APPROVAL_KIND_UNKNOWN`,
43
+ * `APPROVAL_POLICY_LOOSENS`, or `APPROVAL_INVALID`.
44
+ */
45
+ save(): Promise<this>;
46
+ }
47
+ //# sourceMappingURL=ApprovalPolicy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ApprovalPolicy.d.ts","sourceRoot":"","sources":["../../src/models/ApprovalPolicy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAEL,UAAU,EACV,KAAK,iBAAiB,EAEvB,MAAM,0BAA0B,CAAC;AAIlC,sDAAsD;AACtD,MAAM,WAAW,qBAAsB,SAAQ,iBAAiB;IAC9D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,gFAAgF;AAChF,qBAca,cAAe,SAAQ,UAAU;IAC5C,qBAAqB;IAErB,QAAQ,EAAE,MAAM,CAAM;IAEtB,2CAA2C;IAE3C,IAAI,EAAE,MAAM,CAAM;IAElB,oDAAoD;IAEpD,iBAAiB,EAAE,MAAM,CAAK;IAE9B,yEAAyE;IAEzE,KAAK,EAAE,MAAM,CAAK;IAElB,iEAAiE;IAEjE,kBAAkB,EAAE,MAAM,CAAM;IAEhC,4CAA4C;IAE5C,SAAS,EAAE,MAAM,CAAM;gBAEX,OAAO,GAAE,qBAA0B;IAY/C;;;;;;OAMG;IACY,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAWrC"}
@@ -0,0 +1,118 @@
1
+ /**
2
+ * ApprovalRequest: one decision gate on one revision of one subject.
3
+ *
4
+ * The row is the authority for the decision; a consumer's own status column
5
+ * is a projection it writes in the same transaction as `consume`. Every
6
+ * lifecycle field is changed only by `ApprovalService` through one guarded
7
+ * conditional UPDATE, so the generated surface is read-only and a plain
8
+ * `save()` of a loaded request is refused.
9
+ *
10
+ * @packageDocumentation
11
+ */
12
+ import { SmrtObject, type SmrtObjectOptions } from '@happyvertical/smrt-core';
13
+ import { type ApprovalPrincipalType, type ApprovalStatus } from '../types.js';
14
+ /** Constructor options for {@link ApprovalRequest}. */
15
+ export interface ApprovalRequestOptions extends SmrtObjectOptions {
16
+ tenantId?: string;
17
+ kind?: string;
18
+ subjectType?: string;
19
+ subjectId?: string;
20
+ subjectRevisionHash?: string;
21
+ requestKey?: string;
22
+ requestedBy?: string;
23
+ requestedByType?: ApprovalPrincipalType;
24
+ summary?: string;
25
+ status?: ApprovalStatus;
26
+ requiredApprovals?: number;
27
+ approvalCount?: number;
28
+ requiredPermissions?: string;
29
+ version?: number;
30
+ expiresAt?: Date | null;
31
+ decidedAt?: Date | null;
32
+ consumedAt?: Date | null;
33
+ consumedBy?: string;
34
+ }
35
+ /**
36
+ * A request for approval of one subject revision.
37
+ *
38
+ * Status moves `pending` to `approved`, `rejected`, `changes_requested`,
39
+ * `cancelled`, or `expired`; an approved request is consumed once
40
+ * (`consumedAt` set, status stays `approved`) or expires unconsumed.
41
+ */
42
+ export declare class ApprovalRequest extends SmrtObject {
43
+ /** Owning tenant. Every read and transition is pinned to it. */
44
+ tenantId: string;
45
+ /** Approval kind key, e.g. `social.post.publish`. */
46
+ kind: string;
47
+ /** Qualified subject class, e.g. `@happyvertical/smrt-social:SocialPost`. */
48
+ subjectType: string;
49
+ /** Subject id. Bare string: the subject table is polymorphic. */
50
+ subjectId: string;
51
+ /**
52
+ * Hash of the exact subject revision being approved. `consume` must
53
+ * present the same hash, so an approval never transfers to changed content.
54
+ */
55
+ subjectRevisionHash: string;
56
+ /** Idempotency key, unique per tenant. A replay returns the same request. */
57
+ requestKey: string;
58
+ /** Principal id of the requester. The requester can never decide. */
59
+ requestedBy: string;
60
+ /** Principal type of the requester (`human`, `agent`, or `service`). */
61
+ requestedByType: ApprovalPrincipalType;
62
+ /** Short human-readable description shown to deciders. */
63
+ summary: string;
64
+ /** Lifecycle status. Changed only by guarded transitions. */
65
+ status: ApprovalStatus;
66
+ /** Distinct approvers needed, fixed when the request is made. */
67
+ requiredApprovals: number;
68
+ /** Distinct approvals recorded so far. */
69
+ approvalCount: number;
70
+ /**
71
+ * JSON array of permission slugs a decider must hold, fixed at request
72
+ * time. Read it through {@link getRequiredPermissions}.
73
+ */
74
+ requiredPermissions: string;
75
+ /** Transition counter; every guarded UPDATE requires and bumps it. */
76
+ version: number;
77
+ /** After this instant the request can no longer be decided or consumed. */
78
+ expiresAt: Date | null;
79
+ /** When the request reached a terminal decision. */
80
+ decidedAt: Date | null;
81
+ /** When the approval was consumed (single use). */
82
+ consumedAt: Date | null;
83
+ /** Principal id that consumed the approval. */
84
+ consumedBy: string;
85
+ constructor(options?: ApprovalRequestOptions);
86
+ /** The permission slugs a decider must hold (never throws; bad JSON = none valid). */
87
+ getRequiredPermissions(): string[];
88
+ /** Store the permission slugs a decider must hold. */
89
+ setRequiredPermissions(slugs: readonly string[]): void;
90
+ /** The expiry as epoch milliseconds (`NaN` when unset or unparseable). */
91
+ expiresAtMs(): number;
92
+ /** True when the approval was consumed. */
93
+ isConsumed(): boolean;
94
+ /**
95
+ * Refuse any insert `ApprovalService` did not mint (see
96
+ * `write-capability.ts`), and any minted insert that does not start in the
97
+ * initial state. Runs for ordinary saves and bulk creates alike (core
98
+ * wraps an error thrown here; `save()` checks first so callers see it).
99
+ *
100
+ * @throws {ApprovalError} `APPROVAL_FORBIDDEN`.
101
+ */
102
+ protected validateBeforeSave(): Promise<void>;
103
+ private assertInsertAllowed;
104
+ /**
105
+ * Insert a new request. A loaded request is never re-saved: every later
106
+ * change is a guarded transition in `ApprovalService`.
107
+ *
108
+ * @throws {ApprovalError} `APPROVAL_FORBIDDEN` for a persisted request.
109
+ */
110
+ save(): Promise<this>;
111
+ /**
112
+ * Requests are durable records of a decision.
113
+ *
114
+ * @throws {ApprovalError} always.
115
+ */
116
+ delete(): Promise<void>;
117
+ }
118
+ //# sourceMappingURL=ApprovalRequest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ApprovalRequest.d.ts","sourceRoot":"","sources":["../../src/models/ApprovalRequest.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAEL,UAAU,EACV,KAAK,iBAAiB,EAEvB,MAAM,0BAA0B,CAAC;AAElC,OAAO,EAEL,KAAK,qBAAqB,EAC1B,KAAK,cAAc,EACpB,MAAM,aAAa,CAAC;AAGrB,uDAAuD;AACvD,MAAM,WAAW,sBAAuB,SAAQ,iBAAiB;IAC/D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,eAAe,CAAC,EAAE,qBAAqB,CAAC;IACxC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,cAAc,CAAC;IACxB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;IACxB,SAAS,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;IACxB,UAAU,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;GAMG;AACH,qBAsBa,eAAgB,SAAQ,UAAU;IAC7C,gEAAgE;IAEhE,QAAQ,EAAE,MAAM,CAAM;IAEtB,qDAAqD;IAErD,IAAI,EAAE,MAAM,CAAM;IAElB,6EAA6E;IAE7E,WAAW,EAAE,MAAM,CAAM;IAEzB,iEAAiE;IAEjE,SAAS,EAAE,MAAM,CAAM;IAEvB;;;OAGG;IAEH,mBAAmB,EAAE,MAAM,CAAM;IAEjC,6EAA6E;IAE7E,UAAU,EAAE,MAAM,CAAM;IAExB,qEAAqE;IAErE,WAAW,EAAE,MAAM,CAAM;IAEzB,wEAAwE;IAExE,eAAe,EAAE,qBAAqB,CAAW;IAEjD,0DAA0D;IAE1D,OAAO,EAAE,MAAM,CAAM;IAErB,6DAA6D;IAE7D,MAAM,EAAE,cAAc,CAAa;IAEnC,iEAAiE;IAEjE,iBAAiB,EAAE,MAAM,CAAK;IAE9B,0CAA0C;IAE1C,aAAa,EAAE,MAAM,CAAK;IAE1B;;;OAGG;IAEH,mBAAmB,EAAE,MAAM,CAAQ;IAEnC,sEAAsE;IAEtE,OAAO,EAAE,MAAM,CAAK;IAEpB,2EAA2E;IAE3E,SAAS,EAAE,IAAI,GAAG,IAAI,CAAQ;IAE9B,oDAAoD;IAEpD,SAAS,EAAE,IAAI,GAAG,IAAI,CAAQ;IAE9B,mDAAmD;IAEnD,UAAU,EAAE,IAAI,GAAG,IAAI,CAAQ;IAE/B,+CAA+C;IAE/C,UAAU,EAAE,MAAM,CAAM;gBAEZ,OAAO,GAAE,sBAA2B;IA8BhD,sFAAsF;IACtF,sBAAsB,IAAI,MAAM,EAAE;IAWlC,sDAAsD;IACtD,sBAAsB,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI;IAItD,0EAA0E;IAC1E,WAAW,IAAI,MAAM;IASrB,2CAA2C;IAC3C,UAAU,IAAI,OAAO;IAIrB;;;;;;;OAOG;cACsB,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC;IAK5D,OAAO,CAAC,mBAAmB;IAuB3B;;;;;OAKG;IACY,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAYpC;;;;OAIG;IACY,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;CAMvC"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Permission slugs contributed by `@happyvertical/smrt-approvals` to the
3
+ * runtime permission catalog. Deciding a request is gated by the per-kind
4
+ * slug a package passes to `defineApprovalKind`; these slugs cover the
5
+ * package-wide operations.
6
+ *
7
+ * @packageDocumentation
8
+ */
9
+ import { type PermissionDefinition } from '@happyvertical/smrt-users';
10
+ /** Cancel a pending request someone else made. */
11
+ export declare const CANCEL_ANY_APPROVAL_PERMISSION = "approvals.cancel-any";
12
+ /** Write tenant approval policy rows (tighten-only). */
13
+ export declare const MANAGE_APPROVAL_POLICY_PERMISSION = "approvals.manage-policy";
14
+ /** Catalog definitions for the package-wide slugs. */
15
+ export declare const APPROVAL_PERMISSION_DEFINITIONS: PermissionDefinition[];
16
+ /** Register the package slugs (returns an unregister function for tests). */
17
+ export declare function registerApprovalPermissions(): () => void;
18
+ /** Register the package slugs once per process (package entry side effect). */
19
+ export declare function ensureApprovalPermissionsRegistered(): void;
20
+ //# sourceMappingURL=permissions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"permissions.d.ts","sourceRoot":"","sources":["../src/permissions.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EACL,KAAK,oBAAoB,EAE1B,MAAM,2BAA2B,CAAC;AAEnC,kDAAkD;AAClD,eAAO,MAAM,8BAA8B,yBAAyB,CAAC;AAErE,wDAAwD;AACxD,eAAO,MAAM,iCAAiC,4BAA4B,CAAC;AAE3E,sDAAsD;AACtD,eAAO,MAAM,+BAA+B,EAAE,oBAAoB,EAejE,CAAC;AAIF,6EAA6E;AAC7E,wBAAgB,2BAA2B,IAAI,MAAM,IAAI,CAExD;AAED,+EAA+E;AAC/E,wBAAgB,mCAAmC,IAAI,IAAI,CAI1D"}
@@ -0,0 +1,203 @@
1
+ /**
2
+ * ApprovalService: the only write path for approval requests.
3
+ *
4
+ * Every transition is ONE guarded conditional `UPDATE ... WHERE id AND
5
+ * tenant_id AND status AND version AND expiry ... RETURNING version` plus
6
+ * one {@link ApprovalEvent} insert, in the same database transaction (the
7
+ * `CliAuthRequestCollection.approvePendingRequest` and
8
+ * `CommissionPayoutService.transitionPayoutForSource` shape):
9
+ *
10
+ * - The guarded UPDATE is the arbiter. Two concurrent transitions of one
11
+ * request both read version `v`; only one UPDATE can match `version = v`.
12
+ * The loser matches zero rows, writes nothing, and retries from a fresh
13
+ * read, where it sees the winner's state and is refused cleanly
14
+ * (`not_pending`, `already_decided`, `already_consumed`).
15
+ * - PostgreSQL runs each transaction on its own pooled connection; the
16
+ * UPDATE's row lock serializes writers and READ COMMITTED re-evaluates the
17
+ * predicate after the winner commits. On embedded engines (SQLite,
18
+ * DuckDB) each transaction runs through core's embedded write queue
19
+ * (`withEmbeddedWriteTransaction`), which takes the queue before the
20
+ * connection lock and serializes writers per database; the predicate
21
+ * still decides.
22
+ * - Every statement names the tenant explicitly and the work runs inside
23
+ * `withTenant(principal.tenantId)`, so tenancy interceptors and the raw
24
+ * predicate both pin the row; a cross-tenant id is `not_found`.
25
+ * - Events are inserted through a collection bound to the transaction
26
+ * database (the tx-bound collection pattern), so a failed insert rolls
27
+ * the UPDATE back.
28
+ *
29
+ * @packageDocumentation
30
+ */
31
+ import type { DatabaseInterface } from '@happyvertical/sql';
32
+ import { type ApprovalPolicyRules } from './kinds.js';
33
+ import type { ApprovalEvent } from './models/ApprovalEvent.js';
34
+ import type { ApprovalPolicy } from './models/ApprovalPolicy.js';
35
+ import type { ApprovalRequest } from './models/ApprovalRequest.js';
36
+ import { type ApprovalDecision, type ApprovalKind, type ApprovalPrincipal, type ApprovalTransitionResult } from './types.js';
37
+ /** Options for {@link ApprovalService}. */
38
+ export interface ApprovalServiceOptions {
39
+ /** The database every transition runs against. Must support transactions. */
40
+ db: DatabaseInterface;
41
+ /** Clock override for deterministic tests. */
42
+ now?: () => Date;
43
+ /** Attempts before a contended transition is refused. Default 8. */
44
+ maxAttempts?: number;
45
+ }
46
+ /** Input for {@link ApprovalService.requestApproval}. */
47
+ export interface RequestApprovalInput {
48
+ /** A kind registered with `defineApprovalKind` (or its key). */
49
+ kind: ApprovalKind | string;
50
+ /** The subject's id. */
51
+ subjectId: string;
52
+ /** Hash of the exact subject revision to approve. */
53
+ subjectRevisionHash: string;
54
+ /**
55
+ * Idempotency key, unique per tenant. A replay with the same subject and
56
+ * revision returns the existing request as `already_applied`; a replay with
57
+ * different content is refused `request_key_conflict`. Default: random.
58
+ */
59
+ requestKey?: string;
60
+ /** Short description shown to deciders. */
61
+ summary?: string;
62
+ }
63
+ /** Input for {@link ApprovalService.decide}. */
64
+ export interface DecideInput {
65
+ decision: ApprovalDecision;
66
+ /** Required for `reject` and `request_changes`. */
67
+ reason?: string;
68
+ }
69
+ /** Options for {@link ApprovalService.consume}. */
70
+ export interface ConsumeOptions {
71
+ /**
72
+ * Run inside the caller's open transaction (a `db.transaction()` callback
73
+ * handle) so the consumer's own write commits or rolls back together with
74
+ * the consumption. The caller owns the transaction. On embedded engines
75
+ * (SQLite, DuckDB, libsql) the handle must come from
76
+ * `withEmbeddedWriteTransaction(db, isEmbeddedDatabase(db), fn)` or
77
+ * `SmrtObject.withTransaction()` so it holds core's embedded write queue;
78
+ * a raw `db.transaction()` handle can deadlock against concurrent saves.
79
+ */
80
+ transaction?: DatabaseInterface;
81
+ }
82
+ /** Result type of every transition. */
83
+ export type ApprovalResult = ApprovalTransitionResult<ApprovalRequest>;
84
+ /**
85
+ * The decision gate. Construct it over the application's database; it binds
86
+ * its own collections per call and per transaction.
87
+ */
88
+ export declare class ApprovalService {
89
+ private readonly db;
90
+ private readonly clock;
91
+ private readonly maxAttempts;
92
+ constructor(options: ApprovalServiceOptions);
93
+ /** One request in the principal's tenant, or `null`. */
94
+ getRequest(principal: ApprovalPrincipal, requestId: string): Promise<ApprovalRequest | null>;
95
+ /** A request's events in order, in the principal's tenant. */
96
+ listEvents(principal: ApprovalPrincipal, requestId: string): Promise<ApprovalEvent[]>;
97
+ /**
98
+ * Open a request for approval of one subject revision. Any principal type
99
+ * may request. The quorum, expiry, and required permissions are fixed now
100
+ * from the kind's defaults tightened by the tenant's policy.
101
+ */
102
+ requestApproval(principal: ApprovalPrincipal, input: RequestApprovalInput): Promise<ApprovalResult>;
103
+ /**
104
+ * Record a decision. Only a `human` principal of the request's tenant who
105
+ * is not the requester and holds every required permission may decide,
106
+ * once, while the request is pending and unexpired. `approve` counts
107
+ * toward the quorum of distinct approvers; `reject` and `request_changes`
108
+ * end the request.
109
+ */
110
+ decide(principal: ApprovalPrincipal, requestId: string, input: DecideInput): Promise<ApprovalResult>;
111
+ /**
112
+ * Cancel a pending request. The requester may cancel their own request;
113
+ * anyone else needs `approvals.cancel-any`.
114
+ */
115
+ cancel(principal: ApprovalPrincipal, requestId: string, input?: {
116
+ reason?: string;
117
+ }): Promise<ApprovalResult>;
118
+ /**
119
+ * Mark one request expired: a pending request, or an approved one that
120
+ * was never consumed, whose expiry has passed. Needs no permission (it
121
+ * only records what time already decided), so a job may call it.
122
+ */
123
+ expire(principal: ApprovalPrincipal, requestId: string): Promise<ApprovalResult>;
124
+ /**
125
+ * Expire every due request in the principal's tenant: pending requests
126
+ * and unconsumed approvals past their deadline, oldest expiry first, at
127
+ * most `limit`. Returns the ids this call expired.
128
+ */
129
+ expireDue(principal: ApprovalPrincipal, options?: {
130
+ limit?: number;
131
+ }): Promise<string[]>;
132
+ /**
133
+ * Consume an approval exactly once. The presented hash must equal the
134
+ * revision that was approved; the request must be approved, unconsumed,
135
+ * and unexpired. Any principal type of the tenant may consume (the
136
+ * consumer's own service is the executor). Pass `options.transaction` to
137
+ * make the consumption part of the consumer's own transaction.
138
+ */
139
+ consume(principal: ApprovalPrincipal, requestId: string, subjectRevisionHash: string, options?: ConsumeOptions): Promise<ApprovalResult>;
140
+ /**
141
+ * Write the tenant's tightening of a kind. Needs
142
+ * `approvals.manage-policy`; a rule that would loosen the kind's defaults
143
+ * throws `APPROVAL_POLICY_LOOSENS`. `0`/`''` clear a rule back to the
144
+ * default. Applies to requests made afterwards.
145
+ */
146
+ setPolicy(principal: ApprovalPrincipal, kindKey: ApprovalKind | string, rules: ApprovalPolicyRules): Promise<ApprovalPolicy>;
147
+ /** The effective rules a new request of `kind` would get in the tenant. */
148
+ rulesFor(principal: ApprovalPrincipal, kindKey: ApprovalKind | string): Promise<import("./types.js").EffectiveApprovalRules>;
149
+ /**
150
+ * Run a guarded transition with retry. `body` runs inside a transaction
151
+ * on a fresh read of the request and returns `retry` when its guarded
152
+ * UPDATE matched no row (a concurrent transition won).
153
+ */
154
+ private transition;
155
+ /** True when an active tenant context names a different tenant. */
156
+ private contextMismatch;
157
+ /** The refusal returned when {@link contextMismatch} holds. */
158
+ private static mismatchResult;
159
+ /** Pin the work to the principal's tenant; refuse a conflicting context. */
160
+ private inTenant;
161
+ /**
162
+ * Run `fn` in a transaction through core's embedded write queue.
163
+ *
164
+ * On embedded engines (SQLite, DuckDB, libsql) the transaction takes the
165
+ * queue for its database BEFORE the adapter's connection lock, and the
166
+ * model writes inside it (event inserts, change-feed appends) re-enter that
167
+ * hold. Taking the connection lock first deadlocks against an unrelated
168
+ * save that holds the queue and then opens its own write transaction, and
169
+ * lets other root writes land inside this BEGIN..COMMIT. The queue also
170
+ * serializes these transactions per database in-process, which is what
171
+ * the former per-handle promise chain did, so that chain is gone.
172
+ * PostgreSQL is not queued: each transaction has its own pooled
173
+ * connection and the guarded UPDATE's row lock arbitrates.
174
+ */
175
+ private runTransaction;
176
+ /** A collection bound to a transaction database. */
177
+ private txCollection;
178
+ private loadRequest;
179
+ private loadPolicy;
180
+ /**
181
+ * Every slug a decider must hold now: the slugs fixed at request time
182
+ * plus the kind's and the tenant policy's current ones (tightening only:
183
+ * a later policy can add a slug, never remove one).
184
+ */
185
+ private requiredPermissionsFor;
186
+ private appendEvent;
187
+ /**
188
+ * Defence in depth for `consume`: the request row says `approved`, so the
189
+ * ledger must agree. It needs the `created` event at sequence 1, no
190
+ * terminal event, and approvals from at least `requiredApprovals` distinct
191
+ * human actors other than the requester. Catches a status written around
192
+ * the service (raw SQL); model-layer inserts are already capability-gated.
193
+ */
194
+ private ledgerBacksApproval;
195
+ /**
196
+ * The guarded UPDATE bypasses save hooks, so record the change-feed entry
197
+ * the save path would have written. Best effort, like the save path.
198
+ */
199
+ private recordChange;
200
+ /** Re-read the request after commit (ids only cross the boundary). */
201
+ private toResult;
202
+ }
203
+ //# sourceMappingURL=service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAaH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAI5D,OAAO,EACL,KAAK,mBAAmB,EAKzB,MAAM,YAAY,CAAC;AACpB,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AACjE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAKnE,OAAO,EAEL,KAAK,gBAAgB,EAGrB,KAAK,YAAY,EACjB,KAAK,iBAAiB,EAGtB,KAAK,wBAAwB,EAC9B,MAAM,YAAY,CAAC;AAmCpB,2CAA2C;AAC3C,MAAM,WAAW,sBAAsB;IACrC,6EAA6E;IAC7E,EAAE,EAAE,iBAAiB,CAAC;IACtB,8CAA8C;IAC9C,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;IACjB,oEAAoE;IACpE,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,yDAAyD;AACzD,MAAM,WAAW,oBAAoB;IACnC,gEAAgE;IAChE,IAAI,EAAE,YAAY,GAAG,MAAM,CAAC;IAC5B,wBAAwB;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,qDAAqD;IACrD,mBAAmB,EAAE,MAAM,CAAC;IAC5B;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,2CAA2C;IAC3C,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,gDAAgD;AAChD,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,mDAAmD;IACnD,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,mDAAmD;AACnD,MAAM,WAAW,cAAc;IAC7B;;;;;;;;OAQG;IACH,WAAW,CAAC,EAAE,iBAAiB,CAAC;CACjC;AAED,uCAAuC;AACvC,MAAM,MAAM,cAAc,GAAG,wBAAwB,CAAC,eAAe,CAAC,CAAC;AAmGvE;;;GAGG;AACH,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAC,EAAE,CAA6B;IAChD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAa;IACnC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;gBAEzB,OAAO,EAAE,sBAAsB;IAyB3C,wDAAwD;IAClD,UAAU,CACd,SAAS,EAAE,iBAAiB,EAC5B,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC;IAQlC,8DAA8D;IACxD,UAAU,CACd,SAAS,EAAE,iBAAiB,EAC5B,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,aAAa,EAAE,CAAC;IAiB3B;;;;OAIG;IACG,eAAe,CACnB,SAAS,EAAE,iBAAiB,EAC5B,KAAK,EAAE,oBAAoB,GAC1B,OAAO,CAAC,cAAc,CAAC;IA4G1B;;;;;;OAMG;IACG,MAAM,CACV,SAAS,EAAE,iBAAiB,EAC5B,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,WAAW,GACjB,OAAO,CAAC,cAAc,CAAC;IA+I1B;;;OAGG;IACG,MAAM,CACV,SAAS,EAAE,iBAAiB,EAC5B,SAAS,EAAE,MAAM,EACjB,KAAK,GAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAA;KAAO,GAC9B,OAAO,CAAC,cAAc,CAAC;IAgE1B;;;;OAIG;IACG,MAAM,CACV,SAAS,EAAE,iBAAiB,EAC5B,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,cAAc,CAAC;IAgE1B;;;;OAIG;IACG,SAAS,CACb,SAAS,EAAE,iBAAiB,EAC5B,OAAO,GAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAA;KAAO,GAC/B,OAAO,CAAC,MAAM,EAAE,CAAC;IA4BpB;;;;;;OAMG;IACG,OAAO,CACX,SAAS,EAAE,iBAAiB,EAC5B,SAAS,EAAE,MAAM,EACjB,mBAAmB,EAAE,MAAM,EAC3B,OAAO,GAAE,cAAmB,GAC3B,OAAO,CAAC,cAAc,CAAC;IAmF1B;;;;;OAKG;IACG,SAAS,CACb,SAAS,EAAE,iBAAiB,EAC5B,OAAO,EAAE,YAAY,GAAG,MAAM,EAC9B,KAAK,EAAE,mBAAmB,GACzB,OAAO,CAAC,cAAc,CAAC;IAoE1B,2EAA2E;IACrE,QAAQ,CAAC,SAAS,EAAE,iBAAiB,EAAE,OAAO,EAAE,YAAY,GAAG,MAAM;IAiB3E;;;;OAIG;YACW,UAAU;IAgExB,mEAAmE;IACnE,OAAO,CAAC,eAAe;IASvB,+DAA+D;IAC/D,OAAO,CAAC,MAAM,CAAC,cAAc;IAY7B,4EAA4E;YAC9D,QAAQ;IAatB;;;;;;;;;;;;;OAaG;YACW,cAAc;IAU5B,oDAAoD;YACtC,YAAY;YAWZ,WAAW;YAiBX,UAAU;IAgBxB;;;;OAIG;YACW,sBAAsB;YActB,WAAW;IA+BzB;;;;;;OAMG;YACW,mBAAmB;IAgCjC;;;OAGG;YACW,YAAY;IAkB1B,sEAAsE;YACxD,QAAQ;CAmBvB"}