@happyvertical/smrt-approvals 0.57.36 → 0.57.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +220 -30
- package/dist/index.js.map +1 -1
- package/dist/manifest.json +306 -27
- package/dist/models/ApprovalEvent.d.ts.map +1 -1
- package/dist/models/ApprovalPolicy.d.ts.map +1 -1
- package/dist/models/ApprovalRequest.d.ts.map +1 -1
- package/dist/recipes.d.ts +193 -0
- package/dist/recipes.d.ts.map +1 -0
- package/dist/smrt-knowledge.json +284 -32
- package/package.json +8 -6
package/dist/smrt-knowledge.json
CHANGED
|
@@ -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.
|
|
6
|
+
"packageVersion": "0.57.37",
|
|
7
7
|
"sourceManifestPath": "dist/manifest.json",
|
|
8
8
|
"agentDocPath": "AGENTS.md",
|
|
9
9
|
"sourceHashes": {
|
|
10
|
-
"manifest": "
|
|
11
|
-
"packageJson": "
|
|
12
|
-
"agents": "
|
|
10
|
+
"manifest": "d6a87be6bbf7e1c4a66ea3aea14783cfb33ae333fb2e1297c8fcabf5ba59c186",
|
|
11
|
+
"packageJson": "3ee7489ad0e545abdf4f6c9cd58527f923355564680d65fdf642190f39545980",
|
|
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",
|
|
@@ -296,31 +307,36 @@
|
|
|
296
307
|
"name": "kind",
|
|
297
308
|
"type": "text",
|
|
298
309
|
"required": true,
|
|
299
|
-
"columnType": "TEXT"
|
|
310
|
+
"columnType": "TEXT",
|
|
311
|
+
"description": "The sort of approval these rules apply to."
|
|
300
312
|
},
|
|
301
313
|
{
|
|
302
314
|
"name": "requiredApprovals",
|
|
303
315
|
"type": "integer",
|
|
304
316
|
"required": false,
|
|
305
|
-
"columnType": "INTEGER"
|
|
317
|
+
"columnType": "INTEGER",
|
|
318
|
+
"description": "How many different people must agree. Zero keeps the usual number."
|
|
306
319
|
},
|
|
307
320
|
{
|
|
308
321
|
"name": "ttlMs",
|
|
309
322
|
"type": "integer",
|
|
310
323
|
"required": false,
|
|
311
|
-
"columnType": "INTEGER"
|
|
324
|
+
"columnType": "INTEGER",
|
|
325
|
+
"description": "How long a request stays open, in milliseconds. Zero keeps the usual time."
|
|
312
326
|
},
|
|
313
327
|
{
|
|
314
328
|
"name": "requiredPermission",
|
|
315
329
|
"type": "text",
|
|
316
330
|
"required": false,
|
|
317
|
-
"columnType": "TEXT"
|
|
331
|
+
"columnType": "TEXT",
|
|
332
|
+
"description": "An extra permission a decider must hold. Leave empty for none."
|
|
318
333
|
},
|
|
319
334
|
{
|
|
320
335
|
"name": "updatedBy",
|
|
321
336
|
"type": "text",
|
|
322
337
|
"required": false,
|
|
323
|
-
"columnType": "TEXT"
|
|
338
|
+
"columnType": "TEXT",
|
|
339
|
+
"description": "Who last changed these rules."
|
|
324
340
|
}
|
|
325
341
|
],
|
|
326
342
|
"relationships": [],
|
|
@@ -409,77 +425,88 @@
|
|
|
409
425
|
"type": "text",
|
|
410
426
|
"required": true,
|
|
411
427
|
"columnType": "TEXT",
|
|
412
|
-
"readonly": true
|
|
428
|
+
"readonly": true,
|
|
429
|
+
"description": "What sort of approval this is, such as publishing a post."
|
|
413
430
|
},
|
|
414
431
|
{
|
|
415
432
|
"name": "subjectType",
|
|
416
433
|
"type": "text",
|
|
417
434
|
"required": true,
|
|
418
435
|
"columnType": "TEXT",
|
|
419
|
-
"readonly": true
|
|
436
|
+
"readonly": true,
|
|
437
|
+
"description": "What kind of thing the request is about."
|
|
420
438
|
},
|
|
421
439
|
{
|
|
422
440
|
"name": "subjectId",
|
|
423
441
|
"type": "text",
|
|
424
442
|
"required": true,
|
|
425
443
|
"columnType": "TEXT",
|
|
426
|
-
"readonly": true
|
|
444
|
+
"readonly": true,
|
|
445
|
+
"description": "Which thing the request is about."
|
|
427
446
|
},
|
|
428
447
|
{
|
|
429
448
|
"name": "subjectRevisionHash",
|
|
430
449
|
"type": "text",
|
|
431
450
|
"required": true,
|
|
432
451
|
"columnType": "TEXT",
|
|
433
|
-
"readonly": true
|
|
452
|
+
"readonly": true,
|
|
453
|
+
"description": "A fingerprint of the exact version that was put up for approval. If the thing changes, the approval no longer applies."
|
|
434
454
|
},
|
|
435
455
|
{
|
|
436
456
|
"name": "requestKey",
|
|
437
457
|
"type": "text",
|
|
438
458
|
"required": true,
|
|
439
459
|
"columnType": "TEXT",
|
|
440
|
-
"readonly": true
|
|
460
|
+
"readonly": true,
|
|
461
|
+
"description": "The reference the request is filed under, so asking twice does not open it twice."
|
|
441
462
|
},
|
|
442
463
|
{
|
|
443
464
|
"name": "requestedBy",
|
|
444
465
|
"type": "text",
|
|
445
466
|
"required": true,
|
|
446
467
|
"columnType": "TEXT",
|
|
447
|
-
"readonly": true
|
|
468
|
+
"readonly": true,
|
|
469
|
+
"description": "Who asked for the approval."
|
|
448
470
|
},
|
|
449
471
|
{
|
|
450
472
|
"name": "requestedByType",
|
|
451
473
|
"type": "text",
|
|
452
474
|
"required": false,
|
|
453
475
|
"columnType": "TEXT",
|
|
454
|
-
"readonly": true
|
|
476
|
+
"readonly": true,
|
|
477
|
+
"description": "Whether a person, an assistant or an automated process asked."
|
|
455
478
|
},
|
|
456
479
|
{
|
|
457
480
|
"name": "summary",
|
|
458
481
|
"type": "text",
|
|
459
482
|
"required": false,
|
|
460
483
|
"columnType": "TEXT",
|
|
461
|
-
"readonly": true
|
|
484
|
+
"readonly": true,
|
|
485
|
+
"description": "A short description of what needs approval."
|
|
462
486
|
},
|
|
463
487
|
{
|
|
464
488
|
"name": "status",
|
|
465
489
|
"type": "text",
|
|
466
490
|
"required": false,
|
|
467
491
|
"columnType": "TEXT",
|
|
468
|
-
"readonly": true
|
|
492
|
+
"readonly": true,
|
|
493
|
+
"description": "Where the request stands: waiting, approved, rejected, changes requested, cancelled or expired."
|
|
469
494
|
},
|
|
470
495
|
{
|
|
471
496
|
"name": "requiredApprovals",
|
|
472
497
|
"type": "integer",
|
|
473
498
|
"required": false,
|
|
474
499
|
"columnType": "INTEGER",
|
|
475
|
-
"readonly": true
|
|
500
|
+
"readonly": true,
|
|
501
|
+
"description": "How many different people must agree."
|
|
476
502
|
},
|
|
477
503
|
{
|
|
478
504
|
"name": "approvalCount",
|
|
479
505
|
"type": "integer",
|
|
480
506
|
"required": false,
|
|
481
507
|
"columnType": "INTEGER",
|
|
482
|
-
"readonly": true
|
|
508
|
+
"readonly": true,
|
|
509
|
+
"description": "How many people have agreed so far."
|
|
483
510
|
},
|
|
484
511
|
{
|
|
485
512
|
"name": "requiredPermissions",
|
|
@@ -500,28 +527,32 @@
|
|
|
500
527
|
"type": "datetime",
|
|
501
528
|
"required": false,
|
|
502
529
|
"columnType": "TIMESTAMP",
|
|
503
|
-
"readonly": true
|
|
530
|
+
"readonly": true,
|
|
531
|
+
"description": "The deadline. After it the request can no longer be decided."
|
|
504
532
|
},
|
|
505
533
|
{
|
|
506
534
|
"name": "decidedAt",
|
|
507
535
|
"type": "datetime",
|
|
508
536
|
"required": false,
|
|
509
537
|
"columnType": "TIMESTAMP",
|
|
510
|
-
"readonly": true
|
|
538
|
+
"readonly": true,
|
|
539
|
+
"description": "When the request was decided."
|
|
511
540
|
},
|
|
512
541
|
{
|
|
513
542
|
"name": "consumedAt",
|
|
514
543
|
"type": "datetime",
|
|
515
544
|
"required": false,
|
|
516
545
|
"columnType": "TIMESTAMP",
|
|
517
|
-
"readonly": true
|
|
546
|
+
"readonly": true,
|
|
547
|
+
"description": "When the approval was used to let the action go ahead."
|
|
518
548
|
},
|
|
519
549
|
{
|
|
520
550
|
"name": "consumedBy",
|
|
521
551
|
"type": "text",
|
|
522
552
|
"required": false,
|
|
523
553
|
"columnType": "TEXT",
|
|
524
|
-
"readonly": true
|
|
554
|
+
"readonly": true,
|
|
555
|
+
"description": "Who used the approval."
|
|
525
556
|
}
|
|
526
557
|
],
|
|
527
558
|
"relationships": [],
|
|
@@ -743,7 +774,7 @@
|
|
|
743
774
|
"polymorphicAssociations": 0,
|
|
744
775
|
"uuidColumns": 14
|
|
745
776
|
},
|
|
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",
|
|
777
|
+
"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
778
|
"moduleDocs": [
|
|
748
779
|
{
|
|
749
780
|
"path": "agents/approvals.md",
|
|
@@ -751,6 +782,227 @@
|
|
|
751
782
|
"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
783
|
}
|
|
753
784
|
],
|
|
785
|
+
"recipes": [
|
|
786
|
+
{
|
|
787
|
+
"id": "approvals.requests",
|
|
788
|
+
"className": "ApprovalsRecipe",
|
|
789
|
+
"label": "Approvals",
|
|
790
|
+
"summary": "See what is waiting for a yes, who decided what, and the rules each kind of approval follows.",
|
|
791
|
+
"synonyms": [
|
|
792
|
+
"approvals",
|
|
793
|
+
"approval requests",
|
|
794
|
+
"sign-off",
|
|
795
|
+
"sign off",
|
|
796
|
+
"authorisation",
|
|
797
|
+
"authorization",
|
|
798
|
+
"review queue",
|
|
799
|
+
"waiting for approval"
|
|
800
|
+
],
|
|
801
|
+
"models": [
|
|
802
|
+
"@happyvertical/smrt-approvals:ApprovalRequest",
|
|
803
|
+
"@happyvertical/smrt-approvals:ApprovalEvent",
|
|
804
|
+
"@happyvertical/smrt-approvals:ApprovalPolicy"
|
|
805
|
+
],
|
|
806
|
+
"nav": [
|
|
807
|
+
{
|
|
808
|
+
"label": "Requests",
|
|
809
|
+
"model": "@happyvertical/smrt-approvals:ApprovalRequest",
|
|
810
|
+
"icon": "fileText",
|
|
811
|
+
"description": "Everything waiting for a yes or already decided, and where each one stands.",
|
|
812
|
+
"noun": "request"
|
|
813
|
+
},
|
|
814
|
+
{
|
|
815
|
+
"label": "History",
|
|
816
|
+
"model": "@happyvertical/smrt-approvals:ApprovalEvent",
|
|
817
|
+
"icon": "book",
|
|
818
|
+
"description": "Who did what on each request, and when.",
|
|
819
|
+
"noun": "history entry"
|
|
820
|
+
},
|
|
821
|
+
{
|
|
822
|
+
"label": "Policies",
|
|
823
|
+
"model": "@happyvertical/smrt-approvals:ApprovalPolicy",
|
|
824
|
+
"icon": "settings",
|
|
825
|
+
"description": "The rules for each kind of approval: how many people must agree and how long a request lasts.",
|
|
826
|
+
"noun": "policy"
|
|
827
|
+
}
|
|
828
|
+
],
|
|
829
|
+
"requires": [],
|
|
830
|
+
"section": {
|
|
831
|
+
"id": "approvals",
|
|
832
|
+
"label": "Approvals",
|
|
833
|
+
"icon": "briefcase",
|
|
834
|
+
"description": "Who has to say yes before something happens."
|
|
835
|
+
},
|
|
836
|
+
"options": {
|
|
837
|
+
"@happyvertical/smrt-approvals:ApprovalRequest": {
|
|
838
|
+
"fields": {
|
|
839
|
+
"summary": {
|
|
840
|
+
"order": 1
|
|
841
|
+
},
|
|
842
|
+
"kind": {
|
|
843
|
+
"order": 2
|
|
844
|
+
},
|
|
845
|
+
"status": {
|
|
846
|
+
"order": 3
|
|
847
|
+
},
|
|
848
|
+
"requestedBy": {
|
|
849
|
+
"label": "Requested by",
|
|
850
|
+
"order": 4
|
|
851
|
+
},
|
|
852
|
+
"requestedByType": {
|
|
853
|
+
"label": "Requested by a",
|
|
854
|
+
"order": 5
|
|
855
|
+
},
|
|
856
|
+
"expiresAt": {
|
|
857
|
+
"label": "Expires",
|
|
858
|
+
"order": 6
|
|
859
|
+
},
|
|
860
|
+
"decidedAt": {
|
|
861
|
+
"label": "Decided on",
|
|
862
|
+
"order": 7
|
|
863
|
+
},
|
|
864
|
+
"subjectType": {
|
|
865
|
+
"label": "What it is about",
|
|
866
|
+
"visibility": "advanced"
|
|
867
|
+
},
|
|
868
|
+
"subjectId": {
|
|
869
|
+
"label": "Which one",
|
|
870
|
+
"visibility": "advanced"
|
|
871
|
+
},
|
|
872
|
+
"requiredApprovals": {
|
|
873
|
+
"label": "Approvals needed",
|
|
874
|
+
"visibility": "advanced"
|
|
875
|
+
},
|
|
876
|
+
"approvalCount": {
|
|
877
|
+
"label": "Approvals so far",
|
|
878
|
+
"visibility": "advanced"
|
|
879
|
+
},
|
|
880
|
+
"consumedAt": {
|
|
881
|
+
"label": "Used on",
|
|
882
|
+
"visibility": "advanced"
|
|
883
|
+
},
|
|
884
|
+
"consumedBy": {
|
|
885
|
+
"label": "Used by",
|
|
886
|
+
"visibility": "advanced"
|
|
887
|
+
},
|
|
888
|
+
"tenantId": {
|
|
889
|
+
"visibility": "hidden"
|
|
890
|
+
},
|
|
891
|
+
"subjectRevisionHash": {
|
|
892
|
+
"visibility": "hidden"
|
|
893
|
+
},
|
|
894
|
+
"requestKey": {
|
|
895
|
+
"visibility": "hidden"
|
|
896
|
+
},
|
|
897
|
+
"requiredPermissions": {
|
|
898
|
+
"visibility": "hidden"
|
|
899
|
+
},
|
|
900
|
+
"version": {
|
|
901
|
+
"visibility": "hidden"
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
},
|
|
905
|
+
"@happyvertical/smrt-approvals:ApprovalEvent": {
|
|
906
|
+
"fields": {
|
|
907
|
+
"requestId": {
|
|
908
|
+
"label": "Request",
|
|
909
|
+
"order": 1
|
|
910
|
+
},
|
|
911
|
+
"type": {
|
|
912
|
+
"label": "What happened",
|
|
913
|
+
"order": 2
|
|
914
|
+
},
|
|
915
|
+
"actorId": {
|
|
916
|
+
"label": "Who",
|
|
917
|
+
"order": 3
|
|
918
|
+
},
|
|
919
|
+
"actorType": {
|
|
920
|
+
"label": "Who it was",
|
|
921
|
+
"order": 4
|
|
922
|
+
},
|
|
923
|
+
"reason": {
|
|
924
|
+
"order": 5
|
|
925
|
+
},
|
|
926
|
+
"occurredAt": {
|
|
927
|
+
"label": "When",
|
|
928
|
+
"order": 6
|
|
929
|
+
},
|
|
930
|
+
"selfApproved": {
|
|
931
|
+
"label": "Decided by the requester",
|
|
932
|
+
"visibility": "advanced"
|
|
933
|
+
},
|
|
934
|
+
"tenantId": {
|
|
935
|
+
"visibility": "hidden"
|
|
936
|
+
},
|
|
937
|
+
"subjectRevisionHash": {
|
|
938
|
+
"visibility": "hidden"
|
|
939
|
+
},
|
|
940
|
+
"sequence": {
|
|
941
|
+
"visibility": "hidden"
|
|
942
|
+
},
|
|
943
|
+
"voteKey": {
|
|
944
|
+
"visibility": "hidden"
|
|
945
|
+
},
|
|
946
|
+
"selfApprovalPermission": {
|
|
947
|
+
"visibility": "hidden"
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
},
|
|
951
|
+
"@happyvertical/smrt-approvals:ApprovalPolicy": {
|
|
952
|
+
"fields": {
|
|
953
|
+
"kind": {
|
|
954
|
+
"order": 1
|
|
955
|
+
},
|
|
956
|
+
"requiredApprovals": {
|
|
957
|
+
"label": "Approvals needed",
|
|
958
|
+
"order": 2
|
|
959
|
+
},
|
|
960
|
+
"ttlMs": {
|
|
961
|
+
"label": "Time limit in milliseconds",
|
|
962
|
+
"order": 3
|
|
963
|
+
},
|
|
964
|
+
"requiredPermission": {
|
|
965
|
+
"label": "Extra permission needed",
|
|
966
|
+
"order": 4
|
|
967
|
+
},
|
|
968
|
+
"updatedBy": {
|
|
969
|
+
"label": "Last changed by",
|
|
970
|
+
"order": 5
|
|
971
|
+
},
|
|
972
|
+
"tenantId": {
|
|
973
|
+
"visibility": "hidden"
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
},
|
|
978
|
+
"help": {
|
|
979
|
+
"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",
|
|
980
|
+
"fieldRefs": [
|
|
981
|
+
"ApprovalEvent.actorId",
|
|
982
|
+
"ApprovalEvent.occurredAt",
|
|
983
|
+
"ApprovalEvent.reason",
|
|
984
|
+
"ApprovalEvent.requestId",
|
|
985
|
+
"ApprovalEvent.type",
|
|
986
|
+
"ApprovalPolicy.kind",
|
|
987
|
+
"ApprovalPolicy.requiredApprovals",
|
|
988
|
+
"ApprovalPolicy.requiredPermission",
|
|
989
|
+
"ApprovalPolicy.ttlMs",
|
|
990
|
+
"ApprovalRequest.decidedAt",
|
|
991
|
+
"ApprovalRequest.expiresAt",
|
|
992
|
+
"ApprovalRequest.kind",
|
|
993
|
+
"ApprovalRequest.requestedBy",
|
|
994
|
+
"ApprovalRequest.status",
|
|
995
|
+
"ApprovalRequest.summary"
|
|
996
|
+
]
|
|
997
|
+
},
|
|
998
|
+
"demo": {
|
|
999
|
+
"mode": "server",
|
|
1000
|
+
"reasons": [
|
|
1001
|
+
"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)."
|
|
1002
|
+
]
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
],
|
|
754
1006
|
"browser": {
|
|
755
1007
|
"status": "server-only",
|
|
756
1008
|
"issues": [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@happyvertical/smrt-approvals",
|
|
3
|
-
"version": "0.57.
|
|
3
|
+
"version": "0.57.37",
|
|
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.
|
|
24
|
-
"@happyvertical/smrt-tenancy": "0.57.
|
|
25
|
-
"@happyvertical/smrt-users": "0.57.
|
|
26
|
-
"@happyvertical/sql": "^0.
|
|
23
|
+
"@happyvertical/smrt-core": "0.57.37",
|
|
24
|
+
"@happyvertical/smrt-tenancy": "0.57.37",
|
|
25
|
+
"@happyvertical/smrt-users": "0.57.37",
|
|
26
|
+
"@happyvertical/sql": "^0.110.0"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
|
-
"@happyvertical/smrt-
|
|
29
|
+
"@happyvertical/smrt-scanner": "0.57.37",
|
|
30
|
+
"@happyvertical/smrt-types": "0.57.37",
|
|
31
|
+
"@happyvertical/smrt-vitest": "0.57.37",
|
|
30
32
|
"@types/node": "24.13.6",
|
|
31
33
|
"typescript": "6.0.3",
|
|
32
34
|
"vite": "8.1.6",
|