@happyvertical/smrt-approvals 0.57.36 → 0.57.38

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.
@@ -3,13 +3,13 @@
3
3
  "sensitiveFieldsExcluded": true,
4
4
  "generatedAt": "1970-01-01T00:00:00.000Z",
5
5
  "packageName": "@happyvertical/smrt-approvals",
6
- "packageVersion": "0.57.36",
6
+ "packageVersion": "0.57.38",
7
7
  "sourceManifestPath": "dist/manifest.json",
8
8
  "agentDocPath": "AGENTS.md",
9
9
  "sourceHashes": {
10
- "manifest": "8cc50def94858a70514e5f15006c8ddc9ce11d1d083567580b9b0de9db11cccc",
11
- "packageJson": "4be0f31488269382ec877742b1ed6c24fd3d608f1c0c94aefd3573741f04ebac",
12
- "agents": "d97f97c724891f846420dadc8abfc9b73af5e2d453a56ae748816cf1b039d452",
10
+ "manifest": "d4defe973635c9414b7526ce0fe5a39074e481293204900d0c28cd81568c2c3d",
11
+ "packageJson": "0558457aa3754fe7df51a9a056dada3e4cc91649e9c485889b7633d2a8c4c466",
12
+ "agents": "aba637bcff50dd513fd0d2039af397b317cc27e708285aa10d5645e57c08eb53",
13
13
  "moduleDoc:agents/approvals.md": "3e2b9ef6441798041de30d39b295886c758053185085174bd430d2f156b81e5a"
14
14
  },
15
15
  "exports": [
@@ -22,6 +22,8 @@
22
22
  "@happyvertical/smrt-tenancy": "workspace:*",
23
23
  "@happyvertical/smrt-users": "workspace:*",
24
24
  "@happyvertical/sql": "catalog:",
25
+ "@happyvertical/smrt-scanner": "workspace:*",
26
+ "@happyvertical/smrt-types": "workspace:*",
25
27
  "@happyvertical/smrt-vitest": "workspace:*",
26
28
  "@types/node": "24.13.6",
27
29
  "typescript": "6.0.3",
@@ -30,7 +32,9 @@
30
32
  },
31
33
  "smrtDependencies": [
32
34
  "@happyvertical/smrt-core",
35
+ "@happyvertical/smrt-scanner",
33
36
  "@happyvertical/smrt-tenancy",
37
+ "@happyvertical/smrt-types",
34
38
  "@happyvertical/smrt-users",
35
39
  "@happyvertical/smrt-vitest"
36
40
  ],
@@ -121,35 +125,40 @@
121
125
  "type": "foreignKey",
122
126
  "required": true,
123
127
  "related": "ApprovalRequest",
124
- "columnType": "UUID"
128
+ "columnType": "UUID",
129
+ "description": "The request this entry belongs to."
125
130
  },
126
131
  {
127
132
  "name": "type",
128
133
  "type": "text",
129
134
  "required": true,
130
135
  "columnType": "TEXT",
131
- "readonly": true
136
+ "readonly": true,
137
+ "description": "What happened: created, approved, rejected, changes requested, cancelled, expired or used."
132
138
  },
133
139
  {
134
140
  "name": "actorId",
135
141
  "type": "text",
136
142
  "required": true,
137
143
  "columnType": "TEXT",
138
- "readonly": true
144
+ "readonly": true,
145
+ "description": "Who did it."
139
146
  },
140
147
  {
141
148
  "name": "actorType",
142
149
  "type": "text",
143
150
  "required": false,
144
151
  "columnType": "TEXT",
145
- "readonly": true
152
+ "readonly": true,
153
+ "description": "Whether a person, an assistant or an automated process did it."
146
154
  },
147
155
  {
148
156
  "name": "reason",
149
157
  "type": "text",
150
158
  "required": false,
151
159
  "columnType": "TEXT",
152
- "readonly": true
160
+ "readonly": true,
161
+ "description": "The reason given, for example why a request was rejected or cancelled."
153
162
  },
154
163
  {
155
164
  "name": "subjectRevisionHash",
@@ -177,7 +186,8 @@
177
186
  "type": "datetime",
178
187
  "required": false,
179
188
  "columnType": "TIMESTAMP",
180
- "readonly": true
189
+ "readonly": true,
190
+ "description": "When it happened."
181
191
  },
182
192
  {
183
193
  "name": "selfApproved",
@@ -185,7 +195,8 @@
185
195
  "required": false,
186
196
  "columnType": "BOOLEAN",
187
197
  "default": false,
188
- "readonly": true
198
+ "readonly": true,
199
+ "description": "Whether the person who asked also decided, where that is allowed."
189
200
  },
190
201
  {
191
202
  "name": "selfApprovalPermission",
@@ -218,6 +229,9 @@
218
229
  {
219
230
  "name": "save",
220
231
  "async": true,
232
+ "params": [
233
+ "options?: SmrtSaveOptions"
234
+ ],
221
235
  "returns": "Promise<this>"
222
236
  }
223
237
  ],
@@ -296,31 +310,36 @@
296
310
  "name": "kind",
297
311
  "type": "text",
298
312
  "required": true,
299
- "columnType": "TEXT"
313
+ "columnType": "TEXT",
314
+ "description": "The sort of approval these rules apply to."
300
315
  },
301
316
  {
302
317
  "name": "requiredApprovals",
303
318
  "type": "integer",
304
319
  "required": false,
305
- "columnType": "INTEGER"
320
+ "columnType": "INTEGER",
321
+ "description": "How many different people must agree. Zero keeps the usual number."
306
322
  },
307
323
  {
308
324
  "name": "ttlMs",
309
325
  "type": "integer",
310
326
  "required": false,
311
- "columnType": "INTEGER"
327
+ "columnType": "INTEGER",
328
+ "description": "How long a request stays open, in milliseconds. Zero keeps the usual time."
312
329
  },
313
330
  {
314
331
  "name": "requiredPermission",
315
332
  "type": "text",
316
333
  "required": false,
317
- "columnType": "TEXT"
334
+ "columnType": "TEXT",
335
+ "description": "An extra permission a decider must hold. Leave empty for none."
318
336
  },
319
337
  {
320
338
  "name": "updatedBy",
321
339
  "type": "text",
322
340
  "required": false,
323
- "columnType": "TEXT"
341
+ "columnType": "TEXT",
342
+ "description": "Who last changed these rules."
324
343
  }
325
344
  ],
326
345
  "relationships": [],
@@ -331,6 +350,9 @@
331
350
  {
332
351
  "name": "save",
333
352
  "async": true,
353
+ "params": [
354
+ "options?: SmrtSaveOptions"
355
+ ],
334
356
  "returns": "Promise<this>"
335
357
  }
336
358
  ],
@@ -409,77 +431,88 @@
409
431
  "type": "text",
410
432
  "required": true,
411
433
  "columnType": "TEXT",
412
- "readonly": true
434
+ "readonly": true,
435
+ "description": "What sort of approval this is, such as publishing a post."
413
436
  },
414
437
  {
415
438
  "name": "subjectType",
416
439
  "type": "text",
417
440
  "required": true,
418
441
  "columnType": "TEXT",
419
- "readonly": true
442
+ "readonly": true,
443
+ "description": "What kind of thing the request is about."
420
444
  },
421
445
  {
422
446
  "name": "subjectId",
423
447
  "type": "text",
424
448
  "required": true,
425
449
  "columnType": "TEXT",
426
- "readonly": true
450
+ "readonly": true,
451
+ "description": "Which thing the request is about."
427
452
  },
428
453
  {
429
454
  "name": "subjectRevisionHash",
430
455
  "type": "text",
431
456
  "required": true,
432
457
  "columnType": "TEXT",
433
- "readonly": true
458
+ "readonly": true,
459
+ "description": "A fingerprint of the exact version that was put up for approval. If the thing changes, the approval no longer applies."
434
460
  },
435
461
  {
436
462
  "name": "requestKey",
437
463
  "type": "text",
438
464
  "required": true,
439
465
  "columnType": "TEXT",
440
- "readonly": true
466
+ "readonly": true,
467
+ "description": "The reference the request is filed under, so asking twice does not open it twice."
441
468
  },
442
469
  {
443
470
  "name": "requestedBy",
444
471
  "type": "text",
445
472
  "required": true,
446
473
  "columnType": "TEXT",
447
- "readonly": true
474
+ "readonly": true,
475
+ "description": "Who asked for the approval."
448
476
  },
449
477
  {
450
478
  "name": "requestedByType",
451
479
  "type": "text",
452
480
  "required": false,
453
481
  "columnType": "TEXT",
454
- "readonly": true
482
+ "readonly": true,
483
+ "description": "Whether a person, an assistant or an automated process asked."
455
484
  },
456
485
  {
457
486
  "name": "summary",
458
487
  "type": "text",
459
488
  "required": false,
460
489
  "columnType": "TEXT",
461
- "readonly": true
490
+ "readonly": true,
491
+ "description": "A short description of what needs approval."
462
492
  },
463
493
  {
464
494
  "name": "status",
465
495
  "type": "text",
466
496
  "required": false,
467
497
  "columnType": "TEXT",
468
- "readonly": true
498
+ "readonly": true,
499
+ "description": "Where the request stands: waiting, approved, rejected, changes requested, cancelled or expired."
469
500
  },
470
501
  {
471
502
  "name": "requiredApprovals",
472
503
  "type": "integer",
473
504
  "required": false,
474
505
  "columnType": "INTEGER",
475
- "readonly": true
506
+ "readonly": true,
507
+ "description": "How many different people must agree."
476
508
  },
477
509
  {
478
510
  "name": "approvalCount",
479
511
  "type": "integer",
480
512
  "required": false,
481
513
  "columnType": "INTEGER",
482
- "readonly": true
514
+ "readonly": true,
515
+ "description": "How many people have agreed so far."
483
516
  },
484
517
  {
485
518
  "name": "requiredPermissions",
@@ -500,28 +533,32 @@
500
533
  "type": "datetime",
501
534
  "required": false,
502
535
  "columnType": "TIMESTAMP",
503
- "readonly": true
536
+ "readonly": true,
537
+ "description": "The deadline. After it the request can no longer be decided."
504
538
  },
505
539
  {
506
540
  "name": "decidedAt",
507
541
  "type": "datetime",
508
542
  "required": false,
509
543
  "columnType": "TIMESTAMP",
510
- "readonly": true
544
+ "readonly": true,
545
+ "description": "When the request was decided."
511
546
  },
512
547
  {
513
548
  "name": "consumedAt",
514
549
  "type": "datetime",
515
550
  "required": false,
516
551
  "columnType": "TIMESTAMP",
517
- "readonly": true
552
+ "readonly": true,
553
+ "description": "When the approval was used to let the action go ahead."
518
554
  },
519
555
  {
520
556
  "name": "consumedBy",
521
557
  "type": "text",
522
558
  "required": false,
523
559
  "columnType": "TEXT",
524
- "readonly": true
560
+ "readonly": true,
561
+ "description": "Who used the approval."
525
562
  }
526
563
  ],
527
564
  "relationships": [],
@@ -554,6 +591,9 @@
554
591
  {
555
592
  "name": "save",
556
593
  "async": true,
594
+ "params": [
595
+ "options?: SmrtSaveOptions"
596
+ ],
557
597
  "returns": "Promise<this>"
558
598
  },
559
599
  {
@@ -743,7 +783,7 @@
743
783
  "polymorphicAssociations": 0,
744
784
  "uuidColumns": 14
745
785
  },
746
- "agentDoc": "# @happyvertical/smrt-approvals\n\nDecision gate plus ledger (slice 1 of #3716): `ApprovalRequest`,\nappend-only `ApprovalEvent`, tighten-only `ApprovalPolicy`, and\n`ApprovalService`, the only write path. Not a workflow engine: no stages, no\nexecutor, no compensation. Read [agents/approvals.md](agents/approvals.md)\nbefore changing a transition, the guard predicates, or the surface.\n\n## Invariants\n\n- Every transition is ONE guarded `UPDATE ... WHERE id AND tenant_id AND\n status AND version AND expires_at ... RETURNING version` plus one event\n insert, in one transaction. Zero rows means a concurrent writer won: retry\n from a fresh read, never write anyway. Keep the predicate the arbiter; the\n in-transaction reads only classify refusals.\n- Only `human` principals decide; requesters additionally need the kind's\n explicit `selfApprovalPermission`; one decision per actor\n (unique `(request_id, vote_key)`); quorum counts distinct approver events.\n Agents and services may request, consume, and expire.\n- Tenant: every statement names `tenant_id`, the work runs in\n `withTenant(principal.tenantId)`, and a conflicting active context is\n refused. Principal tenant ids must be UUIDs.\n- Policy may only tighten (`assertPolicyTightens` on write, `effectiveRules`\n clamps on read). Quorum and expiry are fixed at request time; decide-time\n permissions are the request snapshot plus the current kind and policy slugs.\n- Generated surface is `list`/`get` on REST and MCP, no CLI. A collection's\n `@smrt()` config is applied to its item class by core, so each collection\n repeats its model's surface exactly; changing one without the other opens\n or closes the model. `surface.test.ts` holds this.\n- Models refuse re-save and delete of requests and events. Never add an\n update path for events. Inserts need the service-only capability in\n `src/write-capability.ts`; never export it or mint it outside the service.\n- The event FK is physical on PostgreSQL and SQLite only: DuckDB runs an\n indexed-column UPDATE on a referenced row as delete plus insert.\n- No Node built-ins in `src/` (browser gate); use `globalThis.crypto`.\n\n## Validation\n\n```bash\npnpm --filter @happyvertical/smrt-approvals test\npnpm --filter @happyvertical/smrt-approvals typecheck\npnpm --filter @happyvertical/smrt-approvals test:postgres\n```\n\nThe behaviour suite (`src/__tests__/helpers/approval-suite.ts`) runs on\nSQLite, DuckDB, and, through `test:postgres`, a migrated PostgreSQL database.\n",
786
+ "agentDoc": "# @happyvertical/smrt-approvals\n\nDecision gate plus ledger (slice 1 of #3716): `ApprovalRequest`,\nappend-only `ApprovalEvent`, tighten-only `ApprovalPolicy`, and\n`ApprovalService`, the only write path. Not a workflow engine: no stages, no\nexecutor, no compensation. Read [agents/approvals.md](agents/approvals.md)\nbefore changing a transition, the guard predicates, or the surface.\n\n## Invariants\n\n- Every transition is ONE guarded `UPDATE ... WHERE id AND tenant_id AND\n status AND version AND expires_at ... RETURNING version` plus one event\n insert, in one transaction. Zero rows means a concurrent writer won: retry\n from a fresh read, never write anyway. Keep the predicate the arbiter; the\n in-transaction reads only classify refusals.\n- Only `human` principals decide; requesters additionally need the kind's\n explicit `selfApprovalPermission`; one decision per actor\n (unique `(request_id, vote_key)`); quorum counts distinct approver events.\n Agents and services may request, consume, and expire.\n- Tenant: every statement names `tenant_id`, the work runs in\n `withTenant(principal.tenantId)`, and a conflicting active context is\n refused. Principal tenant ids must be UUIDs.\n- Policy may only tighten (`assertPolicyTightens` on write, `effectiveRules`\n clamps on read). Quorum and expiry are fixed at request time; decide-time\n permissions are the request snapshot plus the current kind and policy slugs.\n- Generated surface is `list`/`get` on REST and MCP, no CLI. A collection's\n `@smrt()` config is applied to its item class by core, so each collection\n repeats its model's surface exactly; changing one without the other opens\n or closes the model. `surface.test.ts` holds this.\n- Models refuse re-save and delete of requests and events. Never add an\n update path for events. Inserts need the service-only capability in\n `src/write-capability.ts`; never export it or mint it outside the service.\n- The event FK is physical on PostgreSQL and SQLite only: DuckDB runs an\n indexed-column UPDATE on a referenced row as delete plus insert.\n- No Node built-ins in `src/` (browser gate); use `globalThis.crypto`.\n- **Recipe `approvals.requests`** (`src/recipes.ts`, help in `src/requests.recipe.md`): lists requests, history and policies for the planner and other catalog hosts, read-only because the service is the only write path. Fields it shows carry `@field({ description })` in plain words; keep them when editing the models.\n\n## Validation\n\n```bash\npnpm --filter @happyvertical/smrt-approvals test\npnpm --filter @happyvertical/smrt-approvals typecheck\npnpm --filter @happyvertical/smrt-approvals test:postgres\n```\n\nThe behaviour suite (`src/__tests__/helpers/approval-suite.ts`) runs on\nSQLite, DuckDB, and, through `test:postgres`, a migrated PostgreSQL database.\n",
747
787
  "moduleDocs": [
748
788
  {
749
789
  "path": "agents/approvals.md",
@@ -751,6 +791,227 @@
751
791
  "content": "# Approvals: transitions, concurrency, and scope\n\nDesign source: the accepted recommendation on smrt#3716. This file records\nthe slice-1 contract (#3741) and what later slices add.\n\n## States and transitions\n\n```\npending --approve (distinct approvers reach quorum)--> approved\napproved --consume (hash matches, unexpired)---------> approved + consumedAt\npending --reject | request_changes-------------------> rejected | changes_requested\npending --cancel (requester or approvals.cancel-any)-> cancelled\npending | approved(unconsumed) --expire (deadline)---> expired\n```\n\n`changes_requested` is terminal in slice 1: the requester opens a new request\nfor the new revision (a new `subjectRevisionHash`). There is no resubmit.\n\nEach transition in `src/service.ts`:\n\n1. Validates input and the principal (`id`, UUID `tenantId`, `type`, `can`).\n2. Refuses a principal whose tenant differs from an active tenant context.\n3. Opens a transaction through core's `withEmbeddedWriteTransaction(db,\n isEmbeddedDatabase(db), fn)`. On embedded engines (SQLite, DuckDB, libsql)\n it takes the embedded write queue for the database first, then the\n adapter's connection lock; the event insert and change-feed append inside\n re-enter that hold. The reverse order (a raw `db.transaction()`) deadlocks\n against an unrelated NULL-tenant save, which holds the queue and then\n opens its own write transaction, until the 30 s transaction-queue timeout,\n and lets other root writes land inside the transaction. The queue also\n serializes transitions per database, so no separate chain exists.\n PostgreSQL is not queued: each transaction gets its own pooled connection.\n4. Reads the request through a transaction-bound collection with an explicit\n `tenantId` filter, and classifies refusals from that read.\n5. Runs the guarded UPDATE with the version it read. Zero rows: the\n transaction body returns `retry`, nothing was written, and the loop starts\n again from a fresh read (up to `maxAttempts`, then `contention`).\n6. Inserts the event with `sequence` = the returned version and appends a\n change-feed entry (the raw UPDATE bypasses save hooks).\n7. Re-reads the request after commit on the service connection.\n\nOn PostgreSQL the UPDATE takes the row lock; under READ COMMITTED a waiting\nUPDATE re-evaluates `version = ?` after the winner commits, so it matches\nnothing. Unique keys `(request_id, sequence)` and `(request_id, vote_key)`\nare a second line: a violation aborts the attempt and it retries.\n\n`consume({ transaction })` runs steps 4 to 6 on the caller's open\ntransaction, once, without retry or serialization; the caller owns commit.\nOn embedded engines that handle must come from `withEmbeddedWriteTransaction`\nor `SmrtObject.withTransaction()`, never a raw `db.transaction()`, for the\nlock-order reason in step 3.\n\n## Raw SQL boundaries\n\nThe guarded UPDATEs are the only raw SQL. They bind every value (no literal\nUUIDs or empty strings), validate the request id as a UUID first, and name\n`tenant_id`. Reads and inserts go through collections so tenancy\ninterceptors, UUID columns, and the change feed behave as for any model.\n\n## Service-only write capability\n\nOnly `ApprovalService` inserts `ApprovalRequest` and `ApprovalEvent` rows\n(events only in the same transaction as the guarded UPDATE they record). It passes the\nmodule-private `SERVICE_WRITE` symbol (`src/write-capability.ts`, not a public\nexport) as a constructor option; the model records that instance in a\n`WeakSet` and refuses, in `save()` and `validateBeforeSave()`, any insert\nwithout it, and any minted insert that does not start pending, undecided,\nand unconsumed. `new ApprovalRequest({ status: 'approved' }).save()`,\n`ApprovalRequestCollection.create(...)`, and a forged `approved` event\n(which would otherwise count toward quorum) are refused and write nothing. A\nsymbol never arrives from JSON, so REST and MCP input cannot carry it (the\nsmrt-reports `runtime-report.ts` pattern). It guards the in-process API,\nnot raw SQL.\n\n`consume` re-checks the ledger before consuming (defence in depth against a\nstatus written around the service): the `created` event at sequence 1, no\nterminal event, and `approved` events from at least `requiredApprovals`\ndistinct human actors; requester events count only with `selfApproved: true`\nand a valid `selfApprovalPermission` audit slug. Otherwise it refuses\n`unbacked_approval`. Quorum counting in `decide` applies the same filter.\n\n## Out of scope here (later slices of #3716)\n\n- Slice 2: Svelte inbox, panel, decision bar; `approvals.requests` recipe.\n- Slice 3: notification adapter (`UserNotificationService.notify`, sourceRef\n `approval:<id>:<event>`), profiles `AuditLog` mirror, expiry and reminder\n job calling `expireDue`.\n- Slice 4: adopters (social, personas, content, subscriptions).\n- Slice 5: agent and MCP request tools (deciding stays human-only).\n- Slice 6: #3667 `pending_approval` hand-off.\n\nKnown gaps: listing every holder of a permission needs a users-package\nhelper (for notifications); a vote stays counted if the approver later loses\nthe permission.\n\n## Permission-gated self-decisions\n\nA kind may opt in with `selfApprovalPermission: 'content.approve-self'`.\nThe requester must be human and hold that permission plus every normal snapshot,\ncurrent kind and tenant-policy decision permission. All three verdicts use this\nallowance. Without the opt-in or grant, `self_approval` remains the refusal.\nSelf-decisions record `selfApproved` and `selfApprovalPermission` on the event;\nordinary events default to false and an empty slug. These fields are service-only,\nnot caller input. Deploy the generated event-column migration before upgrading\nruntime code; existing events default to false/empty and cannot gain self authority.\nLike other votes, an authorized self vote remains counted after permission or kind\nconfiguration changes. It counts once, never bypasses quorum, expiry or tenancy,\nand remains atomic with the guarded transition.\n\nDuckDB deployments on `@happyvertical/sql` 0.103.1 may repeatedly recreate the\nunchanged request/sequence index during migration because index introspection\nretains SQL identifier quotes; see [SDK #1395](https://github.com/happyvertical/sdk/issues/1395).\nThe new audit columns converge and existing ordinary approvals remain consumable.\n"
752
792
  }
753
793
  ],
794
+ "recipes": [
795
+ {
796
+ "id": "approvals.requests",
797
+ "className": "ApprovalsRecipe",
798
+ "label": "Approvals",
799
+ "summary": "See what is waiting for a yes, who decided what, and the rules each kind of approval follows.",
800
+ "synonyms": [
801
+ "approvals",
802
+ "approval requests",
803
+ "sign-off",
804
+ "sign off",
805
+ "authorisation",
806
+ "authorization",
807
+ "review queue",
808
+ "waiting for approval"
809
+ ],
810
+ "models": [
811
+ "@happyvertical/smrt-approvals:ApprovalRequest",
812
+ "@happyvertical/smrt-approvals:ApprovalEvent",
813
+ "@happyvertical/smrt-approvals:ApprovalPolicy"
814
+ ],
815
+ "nav": [
816
+ {
817
+ "label": "Requests",
818
+ "model": "@happyvertical/smrt-approvals:ApprovalRequest",
819
+ "icon": "fileText",
820
+ "description": "Everything waiting for a yes or already decided, and where each one stands.",
821
+ "noun": "request"
822
+ },
823
+ {
824
+ "label": "History",
825
+ "model": "@happyvertical/smrt-approvals:ApprovalEvent",
826
+ "icon": "book",
827
+ "description": "Who did what on each request, and when.",
828
+ "noun": "history entry"
829
+ },
830
+ {
831
+ "label": "Policies",
832
+ "model": "@happyvertical/smrt-approvals:ApprovalPolicy",
833
+ "icon": "settings",
834
+ "description": "The rules for each kind of approval: how many people must agree and how long a request lasts.",
835
+ "noun": "policy"
836
+ }
837
+ ],
838
+ "requires": [],
839
+ "section": {
840
+ "id": "approvals",
841
+ "label": "Approvals",
842
+ "icon": "briefcase",
843
+ "description": "Who has to say yes before something happens."
844
+ },
845
+ "options": {
846
+ "@happyvertical/smrt-approvals:ApprovalRequest": {
847
+ "fields": {
848
+ "summary": {
849
+ "order": 1
850
+ },
851
+ "kind": {
852
+ "order": 2
853
+ },
854
+ "status": {
855
+ "order": 3
856
+ },
857
+ "requestedBy": {
858
+ "label": "Requested by",
859
+ "order": 4
860
+ },
861
+ "requestedByType": {
862
+ "label": "Requested by a",
863
+ "order": 5
864
+ },
865
+ "expiresAt": {
866
+ "label": "Expires",
867
+ "order": 6
868
+ },
869
+ "decidedAt": {
870
+ "label": "Decided on",
871
+ "order": 7
872
+ },
873
+ "subjectType": {
874
+ "label": "What it is about",
875
+ "visibility": "advanced"
876
+ },
877
+ "subjectId": {
878
+ "label": "Which one",
879
+ "visibility": "advanced"
880
+ },
881
+ "requiredApprovals": {
882
+ "label": "Approvals needed",
883
+ "visibility": "advanced"
884
+ },
885
+ "approvalCount": {
886
+ "label": "Approvals so far",
887
+ "visibility": "advanced"
888
+ },
889
+ "consumedAt": {
890
+ "label": "Used on",
891
+ "visibility": "advanced"
892
+ },
893
+ "consumedBy": {
894
+ "label": "Used by",
895
+ "visibility": "advanced"
896
+ },
897
+ "tenantId": {
898
+ "visibility": "hidden"
899
+ },
900
+ "subjectRevisionHash": {
901
+ "visibility": "hidden"
902
+ },
903
+ "requestKey": {
904
+ "visibility": "hidden"
905
+ },
906
+ "requiredPermissions": {
907
+ "visibility": "hidden"
908
+ },
909
+ "version": {
910
+ "visibility": "hidden"
911
+ }
912
+ }
913
+ },
914
+ "@happyvertical/smrt-approvals:ApprovalEvent": {
915
+ "fields": {
916
+ "requestId": {
917
+ "label": "Request",
918
+ "order": 1
919
+ },
920
+ "type": {
921
+ "label": "What happened",
922
+ "order": 2
923
+ },
924
+ "actorId": {
925
+ "label": "Who",
926
+ "order": 3
927
+ },
928
+ "actorType": {
929
+ "label": "Who it was",
930
+ "order": 4
931
+ },
932
+ "reason": {
933
+ "order": 5
934
+ },
935
+ "occurredAt": {
936
+ "label": "When",
937
+ "order": 6
938
+ },
939
+ "selfApproved": {
940
+ "label": "Decided by the requester",
941
+ "visibility": "advanced"
942
+ },
943
+ "tenantId": {
944
+ "visibility": "hidden"
945
+ },
946
+ "subjectRevisionHash": {
947
+ "visibility": "hidden"
948
+ },
949
+ "sequence": {
950
+ "visibility": "hidden"
951
+ },
952
+ "voteKey": {
953
+ "visibility": "hidden"
954
+ },
955
+ "selfApprovalPermission": {
956
+ "visibility": "hidden"
957
+ }
958
+ }
959
+ },
960
+ "@happyvertical/smrt-approvals:ApprovalPolicy": {
961
+ "fields": {
962
+ "kind": {
963
+ "order": 1
964
+ },
965
+ "requiredApprovals": {
966
+ "label": "Approvals needed",
967
+ "order": 2
968
+ },
969
+ "ttlMs": {
970
+ "label": "Time limit in milliseconds",
971
+ "order": 3
972
+ },
973
+ "requiredPermission": {
974
+ "label": "Extra permission needed",
975
+ "order": 4
976
+ },
977
+ "updatedBy": {
978
+ "label": "Last changed by",
979
+ "order": 5
980
+ },
981
+ "tenantId": {
982
+ "visibility": "hidden"
983
+ }
984
+ }
985
+ }
986
+ },
987
+ "help": {
988
+ "markdown": "## Overview\n\nApprovals is where you see who has to say yes before something happens, such\nas publishing a post or sending an order. Each request names what needs\napproval, who asked and where it stands: waiting, approved, rejected, changes\nrequested, cancelled or expired. The history keeps every step taken on a\nrequest, and the policies show the rules each kind of approval follows.\n\n## Tasks\n\nRequests are raised by the app or by an assistant through the approval\nservice, decisions are recorded by the same service, and policies are set by\nan administrator through it. Nothing here is created or changed by hand in the\napp. Here you find a request, see where it stands and read what happened.\n\n### Find a request\n\n1. Open the list of requests.\n2. Look at the **{field:ApprovalRequest.summary}** to see what needs approval, and the **{field:ApprovalRequest.kind}** to see what sort of approval it is.\n3. Check **{field:ApprovalRequest.requestedBy}** to see who asked.\n\n### See where a request stands\n\n1. Open the request.\n2. Its **{field:ApprovalRequest.status}** shows whether it is still waiting, approved, rejected, sent back for changes, cancelled or expired.\n3. **{field:ApprovalRequest.expiresAt}** is the deadline: after it a request can no longer be decided.\n4. Once decided, **{field:ApprovalRequest.decidedAt}** shows when.\n\n### Read the history of a request\n\n1. Open the history.\n2. Find the entries for the **{field:ApprovalEvent.requestId}** you are interested in.\n3. Each entry shows **{field:ApprovalEvent.type}**, who did it in **{field:ApprovalEvent.actorId}**, and **{field:ApprovalEvent.occurredAt}**, oldest first.\n4. A rejection or cancellation carries its **{field:ApprovalEvent.reason}**.\n\n### Read the rules for a kind of approval\n\n1. Open the policies.\n2. Find the **{field:ApprovalPolicy.kind}** you want to know about.\n3. **{field:ApprovalPolicy.requiredApprovals}** is how many different people must agree, and **{field:ApprovalPolicy.ttlMs}** is how long a request stays open.\n4. **{field:ApprovalPolicy.requiredPermission}** names any extra permission a decider must hold.\n",
989
+ "fieldRefs": [
990
+ "ApprovalEvent.actorId",
991
+ "ApprovalEvent.occurredAt",
992
+ "ApprovalEvent.reason",
993
+ "ApprovalEvent.requestId",
994
+ "ApprovalEvent.type",
995
+ "ApprovalPolicy.kind",
996
+ "ApprovalPolicy.requiredApprovals",
997
+ "ApprovalPolicy.requiredPermission",
998
+ "ApprovalPolicy.ttlMs",
999
+ "ApprovalRequest.decidedAt",
1000
+ "ApprovalRequest.expiresAt",
1001
+ "ApprovalRequest.kind",
1002
+ "ApprovalRequest.requestedBy",
1003
+ "ApprovalRequest.status",
1004
+ "ApprovalRequest.summary"
1005
+ ]
1006
+ },
1007
+ "demo": {
1008
+ "mode": "server",
1009
+ "reasons": [
1010
+ "Needs a server because its package depends on @happyvertical/smrt-profiles, @happyvertical/smrt-tenancy, @happyvertical/smrt-users, which do not build for a browser (#3617, #3624, #3626)."
1011
+ ]
1012
+ }
1013
+ }
1014
+ ],
754
1015
  "browser": {
755
1016
  "status": "server-only",
756
1017
  "issues": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@happyvertical/smrt-approvals",
3
- "version": "0.57.36",
3
+ "version": "0.57.38",
4
4
  "description": "Approval decision gate for the SMRT framework: revision-bound, single-use approvals with quorum, expiry, and an append-only event ledger",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -20,13 +20,15 @@
20
20
  "./manifest.json": "./dist/manifest.json"
21
21
  },
22
22
  "dependencies": {
23
- "@happyvertical/smrt-core": "0.57.36",
24
- "@happyvertical/smrt-tenancy": "0.57.36",
25
- "@happyvertical/smrt-users": "0.57.36",
26
- "@happyvertical/sql": "^0.109.0"
23
+ "@happyvertical/smrt-core": "0.57.38",
24
+ "@happyvertical/smrt-tenancy": "0.57.38",
25
+ "@happyvertical/smrt-users": "0.57.38",
26
+ "@happyvertical/sql": "^0.110.0"
27
27
  },
28
28
  "devDependencies": {
29
- "@happyvertical/smrt-vitest": "0.57.36",
29
+ "@happyvertical/smrt-scanner": "0.57.38",
30
+ "@happyvertical/smrt-types": "0.57.38",
31
+ "@happyvertical/smrt-vitest": "0.57.38",
30
32
  "@types/node": "24.13.6",
31
33
  "typescript": "6.0.3",
32
34
  "vite": "8.1.6",