@cortexkit/antigravity-auth-core 3.0.11 → 3.0.12

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 (27) hide show
  1. package/dist/agy-transport.d.ts.map +1 -1
  2. package/dist/agy-transport.js +4 -0
  3. package/dist/agy-transport.js.map +1 -1
  4. package/dist/common-auth-embedded/claustrum/consumer.d.ts +7 -1
  5. package/dist/common-auth-embedded/claustrum/consumer.js +17 -2
  6. package/dist/common-auth-embedded/claustrum/errors.d.ts +1 -1
  7. package/dist/common-auth-embedded/claustrum/index.d.ts +1 -1
  8. package/dist/common-auth-embedded/claustrum/index.js +1 -1
  9. package/dist/common-auth-embedded/claustrum/quota-writer.js +4 -54
  10. package/dist/common-auth-embedded/claustrum/roster.d.ts +31 -0
  11. package/dist/common-auth-embedded/claustrum/roster.js +72 -0
  12. package/dist/common-auth-embedded/commands/builtins.d.ts +37 -0
  13. package/dist/common-auth-embedded/commands/builtins.js +95 -13
  14. package/dist/common-auth-embedded/commands/index.d.ts +1 -1
  15. package/dist/common-auth-embedded/quota/pending.js +48 -0
  16. package/dist/common-auth-embedded/source-output.json +74 -58
  17. package/dist/common-auth-embedded/store/attribution.d.ts +5 -0
  18. package/dist/common-auth-embedded/store/attribution.js +39 -25
  19. package/dist/common-auth-embedded/store/pool.d.ts +17 -0
  20. package/dist/common-auth-embedded/store/pool.js +18 -3
  21. package/dist/common-auth-embedded/store/pull.js +3 -1
  22. package/dist/common-auth-embedded/store/quota-writer.js +298 -0
  23. package/dist/common-auth-embedded/store/runtime.d.ts +2 -0
  24. package/dist/pre-response-deadline.d.ts.map +1 -1
  25. package/dist/pre-response-deadline.js +6 -5
  26. package/dist/pre-response-deadline.js.map +1 -1
  27. package/package.json +1 -1
@@ -0,0 +1,48 @@
1
+ // Use the same independently dated parts as mergeQuotaObservation. A header
2
+ // reading cannot erase a poll-only window, counter, credits record or budget.
3
+ export function quotaFields(publication, witness = '') {
4
+ const result = new Map();
5
+ const { observation } = publication;
6
+ const add = (key, at, part) => result.set(`${witness}:${key}`, {
7
+ at,
8
+ order: 0,
9
+ publication: { ...publication, observation: part },
10
+ });
11
+ const keyFor = (scope, label) => JSON.stringify([scope ?? 'all', label]);
12
+ const read = new Set();
13
+ for (const reading of observation.readings ?? []) {
14
+ const key = keyFor(reading.scope, reading.label);
15
+ read.add(key);
16
+ add(key, observation.checkedAt, {
17
+ checkedAt: observation.checkedAt,
18
+ readings: [{ ...reading }],
19
+ });
20
+ }
21
+ for (const pair of observation.coverage ?? []) {
22
+ const key = keyFor(pair.scope, pair.label);
23
+ if (!read.has(key))
24
+ add(key, observation.checkedAt, {
25
+ checkedAt: observation.checkedAt,
26
+ coverage: [{ ...pair }],
27
+ });
28
+ }
29
+ if (observation.budget)
30
+ add('budget', observation.checkedAt, {
31
+ checkedAt: observation.checkedAt,
32
+ budget: { ...observation.budget },
33
+ });
34
+ for (const [name, counter] of Object.entries(observation.counters ?? {})) {
35
+ add(`counter:${name}`, counter.checkedAt, {
36
+ checkedAt: observation.checkedAt,
37
+ counters: { [name]: { ...counter } },
38
+ });
39
+ }
40
+ if (observation.credits)
41
+ add('credits', observation.credits.checkedAt, {
42
+ checkedAt: observation.checkedAt,
43
+ credits: { ...observation.credits },
44
+ });
45
+ if (!result.size)
46
+ add('empty', observation.checkedAt, { checkedAt: observation.checkedAt });
47
+ return result;
48
+ }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schema": 1,
3
3
  "package": "@cortexkit/common-auth",
4
- "version": "0.24.0",
4
+ "version": "0.26.0",
5
5
  "artifactStatus": "published",
6
6
  "publicRoots": {
7
7
  "./store": {
@@ -37,15 +37,15 @@
37
37
  "import": "./dist/pi-slot/index.js"
38
38
  }
39
39
  },
40
- "input": "tools/common-auth-build/inputs/cortexkit-common-auth-0.24.0.tgz",
41
- "tarballSha256": "7b96e74e4017df1678050b1fe18ca821a548da4412414d4df222307a7c1d0ca0",
42
- "sri": "sha512-dHGHnWBbVC/NHTq22GlFaqYFWg7M1TcCYZvYVIML1bx/rQiIoG05t/ojXjRWdrymHV7GYoaE+yx0YdYHHrPByQ==",
43
- "canonicalFiles": 139,
44
- "canonicalBytes": 861956,
40
+ "input": "tools/common-auth-build/inputs/cortexkit-common-auth-0.26.0.tgz",
41
+ "tarballSha256": "a5a6cb7051ee8c45e86e19cf7a918d0e28ce1b6a80301e982efbc7bb4b98208d",
42
+ "sri": "sha512-qTmQ9aMG+HcyDbhKie8Nq9wxKMRQ6GXH2hKq8Fab9kHqFfzswdp2NpXofGY52RhQ2VxuyMOQr5T6lUrkbny5VQ==",
43
+ "canonicalFiles": 141,
44
+ "canonicalBytes": 888503,
45
45
  "generator": {
46
46
  "path": "packages/opencode/scripts/embed-common-auth.ts",
47
47
  "version": 3,
48
- "sha256": "0d093bc6e05261723f521904c32a328d890fdac5d00bf8b7035708ae44674103"
48
+ "sha256": "89dbeb07d77e0657abeb72a40fcc03a900e98292956865252349a01a15999ed0"
49
49
  },
50
50
  "files": [
51
51
  {
@@ -227,17 +227,17 @@
227
227
  {
228
228
  "source": "package/dist/claustrum/consumer.d.ts",
229
229
  "output": "claustrum/consumer.d.ts",
230
- "bytes": 9548,
231
- "sourceSha256": "150eac9038881740a78e5664501a6ef0aa5e177a6dd46854cbeaf38513463e90",
232
- "outputSha256": "150eac9038881740a78e5664501a6ef0aa5e177a6dd46854cbeaf38513463e90",
230
+ "bytes": 9907,
231
+ "sourceSha256": "8001ce3f928af1bc9124223071eb9c85ea3273436cd7be073c588217f942b248",
232
+ "outputSha256": "8001ce3f928af1bc9124223071eb9c85ea3273436cd7be073c588217f942b248",
233
233
  "transform": "verbatim"
234
234
  },
235
235
  {
236
236
  "source": "package/dist/claustrum/consumer.js",
237
237
  "output": "claustrum/consumer.js",
238
- "bytes": 25698,
239
- "sourceSha256": "ec47dc016bf4d95655c17093a78d9c214b623214c821d352f73ea60f830c0d83",
240
- "outputSha256": "ec47dc016bf4d95655c17093a78d9c214b623214c821d352f73ea60f830c0d83",
238
+ "bytes": 26470,
239
+ "sourceSha256": "b2087accd6e8759fb49173dc744243ed703520d5011386ef514672be8fe79fbe",
240
+ "outputSha256": "b2087accd6e8759fb49173dc744243ed703520d5011386ef514672be8fe79fbe",
241
241
  "transform": "verbatim"
242
242
  },
243
243
  {
@@ -275,9 +275,9 @@
275
275
  {
276
276
  "source": "package/dist/claustrum/errors.d.ts",
277
277
  "output": "claustrum/errors.d.ts",
278
- "bytes": 1145,
279
- "sourceSha256": "b7533d4c281807224d6d05f4bb7a596006862a6c05dc4462fae02af388054bce",
280
- "outputSha256": "b7533d4c281807224d6d05f4bb7a596006862a6c05dc4462fae02af388054bce",
278
+ "bytes": 1163,
279
+ "sourceSha256": "a6e5ec8be3ae7160b78456e642b440494604c2e2cdc987550744521dc873fb40",
280
+ "outputSha256": "a6e5ec8be3ae7160b78456e642b440494604c2e2cdc987550744521dc873fb40",
281
281
  "transform": "verbatim"
282
282
  },
283
283
  {
@@ -307,17 +307,17 @@
307
307
  {
308
308
  "source": "package/dist/claustrum/index.d.ts",
309
309
  "output": "claustrum/index.d.ts",
310
- "bytes": 3761,
311
- "sourceSha256": "f2c1b8d99048666c068805b08b430492b2632be08e2beda466ee56b871572847",
312
- "outputSha256": "f2c1b8d99048666c068805b08b430492b2632be08e2beda466ee56b871572847",
310
+ "bytes": 3844,
311
+ "sourceSha256": "36c0b94c5e18972463f48bfd7e1506d9655372aa8df065786b3a47d199cdb5e0",
312
+ "outputSha256": "36c0b94c5e18972463f48bfd7e1506d9655372aa8df065786b3a47d199cdb5e0",
313
313
  "transform": "verbatim"
314
314
  },
315
315
  {
316
316
  "source": "package/dist/claustrum/index.js",
317
317
  "output": "claustrum/index.js",
318
- "bytes": 2509,
319
- "sourceSha256": "5f8b4bebfc2dec055b69769adcd1ce48ad7acc2c26256edbe4a2e984dc788147",
320
- "outputSha256": "5f8b4bebfc2dec055b69769adcd1ce48ad7acc2c26256edbe4a2e984dc788147",
318
+ "bytes": 2529,
319
+ "sourceSha256": "42c9e67bb5ec6749de488933a56fb3ee253c3b9e3588d692f171d87c59bcbd89",
320
+ "outputSha256": "42c9e67bb5ec6749de488933a56fb3ee253c3b9e3588d692f171d87c59bcbd89",
321
321
  "transform": "verbatim"
322
322
  },
323
323
  {
@@ -363,25 +363,25 @@
363
363
  {
364
364
  "source": "package/dist/claustrum/quota-writer.js",
365
365
  "output": "claustrum/quota-writer.js",
366
- "bytes": 9694,
367
- "sourceSha256": "5a7d5d1059dcadc06aa41e81e0707cc444636473822047f0ec72da7589c770f1",
368
- "outputSha256": "5a7d5d1059dcadc06aa41e81e0707cc444636473822047f0ec72da7589c770f1",
366
+ "bytes": 7579,
367
+ "sourceSha256": "0054a12ca94aab2eecf50f9b5e090879ff184e7f286cc979f32d311e22011906",
368
+ "outputSha256": "0054a12ca94aab2eecf50f9b5e090879ff184e7f286cc979f32d311e22011906",
369
369
  "transform": "verbatim"
370
370
  },
371
371
  {
372
372
  "source": "package/dist/claustrum/roster.d.ts",
373
373
  "output": "claustrum/roster.d.ts",
374
- "bytes": 10931,
375
- "sourceSha256": "c394bc08c7cce4dc68fd539ac03cb200727534693600c7daf5ba7c9c4d0618ce",
376
- "outputSha256": "c394bc08c7cce4dc68fd539ac03cb200727534693600c7daf5ba7c9c4d0618ce",
374
+ "bytes": 12574,
375
+ "sourceSha256": "c9ab3a89e04f72de09c5f15ccf1436b91f8a245e2253f24ec5998120058aebe7",
376
+ "outputSha256": "c9ab3a89e04f72de09c5f15ccf1436b91f8a245e2253f24ec5998120058aebe7",
377
377
  "transform": "verbatim"
378
378
  },
379
379
  {
380
380
  "source": "package/dist/claustrum/roster.js",
381
381
  "output": "claustrum/roster.js",
382
- "bytes": 27602,
383
- "sourceSha256": "45ed2d4e69fad154ce86f5ff4f839c2ae9269dd9146ce7a8b2e5b6f49476ddf8",
384
- "outputSha256": "45ed2d4e69fad154ce86f5ff4f839c2ae9269dd9146ce7a8b2e5b6f49476ddf8",
382
+ "bytes": 30876,
383
+ "sourceSha256": "d02184fd4d32aa8917369919edea4c44328e7332e30f3ddc41ce350e5fd1df07",
384
+ "outputSha256": "d02184fd4d32aa8917369919edea4c44328e7332e30f3ddc41ce350e5fd1df07",
385
385
  "transform": "verbatim"
386
386
  },
387
387
  {
@@ -403,25 +403,25 @@
403
403
  {
404
404
  "source": "package/dist/commands/builtins.d.ts",
405
405
  "output": "commands/builtins.d.ts",
406
- "bytes": 2931,
407
- "sourceSha256": "b70028ea58b51d344e83ac62f36fa95499ab32af753e657ade8116491488bab9",
408
- "outputSha256": "b70028ea58b51d344e83ac62f36fa95499ab32af753e657ade8116491488bab9",
406
+ "bytes": 4576,
407
+ "sourceSha256": "5ab6cbab696c91fef0fc7288d4b82803cd0bac809b68f939d73671505fc1d894",
408
+ "outputSha256": "5ab6cbab696c91fef0fc7288d4b82803cd0bac809b68f939d73671505fc1d894",
409
409
  "transform": "verbatim"
410
410
  },
411
411
  {
412
412
  "source": "package/dist/commands/builtins.js",
413
413
  "output": "commands/builtins.js",
414
- "bytes": 21323,
415
- "sourceSha256": "9b3461ace8a585dde5eff3f9b709ddc766ab3d77e680b687499efd1871a341ce",
416
- "outputSha256": "9b3461ace8a585dde5eff3f9b709ddc766ab3d77e680b687499efd1871a341ce",
414
+ "bytes": 25155,
415
+ "sourceSha256": "c5f7fca0e36dc2f3253b88e7600d3dbed6a193d2b8abbf07d16913b6b46551e6",
416
+ "outputSha256": "c5f7fca0e36dc2f3253b88e7600d3dbed6a193d2b8abbf07d16913b6b46551e6",
417
417
  "transform": "verbatim"
418
418
  },
419
419
  {
420
420
  "source": "package/dist/commands/index.d.ts",
421
421
  "output": "commands/index.d.ts",
422
- "bytes": 1081,
423
- "sourceSha256": "cfe14cdf7ae83a3bf37cfc3e4199c8f80e7720b982a0e875064113b1461c2967",
424
- "outputSha256": "cfe14cdf7ae83a3bf37cfc3e4199c8f80e7720b982a0e875064113b1461c2967",
422
+ "bytes": 1100,
423
+ "sourceSha256": "4be7eec728211b7ad707121b1f2bbe2c219574aaa4e1c5be3f8964b185d4377c",
424
+ "outputSha256": "4be7eec728211b7ad707121b1f2bbe2c219574aaa4e1c5be3f8964b185d4377c",
425
425
  "transform": "verbatim"
426
426
  },
427
427
  {
@@ -776,6 +776,14 @@
776
776
  "outputSha256": "b1d76faf1fb480b97ef1886ce75dbd2156002baedc39249468559cb4eece5bbb",
777
777
  "transform": "verbatim"
778
778
  },
779
+ {
780
+ "source": "package/dist/quota/pending.js",
781
+ "output": "quota/pending.js",
782
+ "bytes": 1876,
783
+ "sourceSha256": "e757a3fa9f8440190668c6154468a5baec8a84e20d6c831a6d7935c3b9bebe29",
784
+ "outputSha256": "e757a3fa9f8440190668c6154468a5baec8a84e20d6c831a6d7935c3b9bebe29",
785
+ "transform": "verbatim"
786
+ },
779
787
  {
780
788
  "source": "package/dist/quota/projection.d.ts",
781
789
  "output": "quota/projection.d.ts",
@@ -875,17 +883,17 @@
875
883
  {
876
884
  "source": "package/dist/store/attribution.d.ts",
877
885
  "output": "store/attribution.d.ts",
878
- "bytes": 1528,
879
- "sourceSha256": "441dba300556a927e872597c1df3e4fbb1ac61c0a3989e98dcd3a94a773b8ed7",
880
- "outputSha256": "441dba300556a927e872597c1df3e4fbb1ac61c0a3989e98dcd3a94a773b8ed7",
886
+ "bytes": 1951,
887
+ "sourceSha256": "16919a55fe944b459ff511c125ac2ba34714949d38db006c8ab047dd48ab51a6",
888
+ "outputSha256": "16919a55fe944b459ff511c125ac2ba34714949d38db006c8ab047dd48ab51a6",
881
889
  "transform": "verbatim"
882
890
  },
883
891
  {
884
892
  "source": "package/dist/store/attribution.js",
885
893
  "output": "store/attribution.js",
886
- "bytes": 2880,
887
- "sourceSha256": "4bda762db5756c0d498d987aa3bc97eeee94be1c1ad2f005ed0ed46785e242d9",
888
- "outputSha256": "4bda762db5756c0d498d987aa3bc97eeee94be1c1ad2f005ed0ed46785e242d9",
894
+ "bytes": 3280,
895
+ "sourceSha256": "5b54980a4f158c9b61cd90c4a83a14e83268fe98101fc88409500bb73d1126bb",
896
+ "outputSha256": "5b54980a4f158c9b61cd90c4a83a14e83268fe98101fc88409500bb73d1126bb",
889
897
  "transform": "verbatim"
890
898
  },
891
899
  {
@@ -971,17 +979,17 @@
971
979
  {
972
980
  "source": "package/dist/store/pool.d.ts",
973
981
  "output": "store/pool.d.ts",
974
- "bytes": 10785,
975
- "sourceSha256": "192ea359d2ce746b31693cf06795a281f2e06804c567975f127ea05fa6fb4aef",
976
- "outputSha256": "192ea359d2ce746b31693cf06795a281f2e06804c567975f127ea05fa6fb4aef",
982
+ "bytes": 11743,
983
+ "sourceSha256": "0693e165ef937c22c375a3f63d01209c844d101ddf1a6702c8acc99bb5fc2a9d",
984
+ "outputSha256": "0693e165ef937c22c375a3f63d01209c844d101ddf1a6702c8acc99bb5fc2a9d",
977
985
  "transform": "verbatim"
978
986
  },
979
987
  {
980
988
  "source": "package/dist/store/pool.js",
981
989
  "output": "store/pool.js",
982
- "bytes": 5671,
983
- "sourceSha256": "8dc0d27b37204c2c3229296e8efbbfac6bcb30538b39d8d95771267487a06292",
984
- "outputSha256": "8dc0d27b37204c2c3229296e8efbbfac6bcb30538b39d8d95771267487a06292",
990
+ "bytes": 6332,
991
+ "sourceSha256": "45a696753691027dfcadd8e94b38b111ad5085ba1cb4f769225b15efc68125d7",
992
+ "outputSha256": "45a696753691027dfcadd8e94b38b111ad5085ba1cb4f769225b15efc68125d7",
985
993
  "transform": "verbatim"
986
994
  },
987
995
  {
@@ -1027,9 +1035,17 @@
1027
1035
  {
1028
1036
  "source": "package/dist/store/pull.js",
1029
1037
  "output": "store/pull.js",
1030
- "bytes": 5470,
1031
- "sourceSha256": "4db913a0d196ed634277e4f3569c275579038f948e4adf6eb4dbbae257a41c92",
1032
- "outputSha256": "4db913a0d196ed634277e4f3569c275579038f948e4adf6eb4dbbae257a41c92",
1038
+ "bytes": 5602,
1039
+ "sourceSha256": "512a723a49dae3be50aa8c67ca8f6a5c165ec72e8a991a166d8d93e33e895350",
1040
+ "outputSha256": "512a723a49dae3be50aa8c67ca8f6a5c165ec72e8a991a166d8d93e33e895350",
1041
+ "transform": "verbatim"
1042
+ },
1043
+ {
1044
+ "source": "package/dist/store/quota-writer.js",
1045
+ "output": "store/quota-writer.js",
1046
+ "bytes": 12396,
1047
+ "sourceSha256": "ade5fdda516011baf11e40f672263364cea07c45bdcbfc11107e22794e9aef21",
1048
+ "outputSha256": "ade5fdda516011baf11e40f672263364cea07c45bdcbfc11107e22794e9aef21",
1033
1049
  "transform": "verbatim"
1034
1050
  },
1035
1051
  {
@@ -1091,9 +1107,9 @@
1091
1107
  {
1092
1108
  "source": "package/dist/store/runtime.d.ts",
1093
1109
  "output": "store/runtime.d.ts",
1094
- "bytes": 1949,
1095
- "sourceSha256": "6838abdf1de68578c8403f76d3404a86e0d286c28a99ff32cb8bd84544a718cb",
1096
- "outputSha256": "6838abdf1de68578c8403f76d3404a86e0d286c28a99ff32cb8bd84544a718cb",
1110
+ "bytes": 2100,
1111
+ "sourceSha256": "34efe7252b6c0aead222cab6164340e76e9c22202049158164c56c3b46bab91f",
1112
+ "outputSha256": "34efe7252b6c0aead222cab6164340e76e9c22202049158164c56c3b46bab91f",
1097
1113
  "transform": "verbatim"
1098
1114
  },
1099
1115
  {
@@ -1,4 +1,6 @@
1
+ import { type StoreContext } from './mutate.js';
1
2
  import { type StoreRuntime } from './runtime.js';
3
+ import { type PoolRow } from './schema.js';
2
4
  /**
3
5
  * What a pull or refresh captured about its row (named by id alongside) when
4
6
  * it was issued. A result applies only while the row with that id still has
@@ -20,6 +22,9 @@ export interface Attribution {
20
22
  credentialEpoch: number;
21
23
  identity?: string;
22
24
  }
25
+ export declare function assertQuotaEpoch(id: string, attribution: Attribution): void;
26
+ export declare function assertQuotaRow(id: string, attribution: Attribution, row: PoolRow | undefined): asserts row is PoolRow;
27
+ export declare function mergedQuota(ctx: StoreContext, id: string, stored: unknown, observation: unknown): unknown;
23
28
  /**
24
29
  * Merges a quota observation into a row's stored map under the store locks,
25
30
  * after attribution passes. Needs no row lock, so it is permitted from
@@ -1,8 +1,41 @@
1
1
  import { PoolOperationError } from './errors.js';
2
- import { toFailure, withTransaction } from './mutate.js';
2
+ import { toFailure, withTransaction, } from './mutate.js';
3
3
  import { LockStack } from './refresh-lock.js';
4
4
  import { refusal, requireBound, unknownRow, } from './runtime.js';
5
5
  import { isCredentialEpoch } from './schema.js';
6
+ export function assertQuotaEpoch(id, attribution) {
7
+ if (!isCredentialEpoch(attribution?.credentialEpoch))
8
+ throw refusal('pull', id, 'invalid-input', 'the credential epoch the reading was issued for must be a positive safe integer');
9
+ }
10
+ // Shared by preflight and the locked commit: accepting into memory must never
11
+ // hide a refusal the immediate writer would have returned to its caller.
12
+ export function assertQuotaRow(id, attribution, row) {
13
+ if (!row)
14
+ throw unknownRow('pull', id);
15
+ if (row.invalid)
16
+ throw refusal('pull', id, 'invalid-row', `row ${id} is invalid`);
17
+ if (!row.credential)
18
+ throw refusal('pull', id, 'no-credential', `row ${id} holds no credential`);
19
+ requireBound('pull', row);
20
+ if (row.torn ||
21
+ !row.hasEntry ||
22
+ row.credentialEpoch !== attribution.credentialEpoch ||
23
+ row.identity !== attribution.identity)
24
+ throw new PoolOperationError({
25
+ operation: 'pull',
26
+ rowId: id,
27
+ phase: 'pull',
28
+ retryable: true,
29
+ kind: 'attribution',
30
+ message: `quota for ${id} was issued for a credential the row no longer holds`,
31
+ });
32
+ }
33
+ export function mergedQuota(ctx, id, stored, observation) {
34
+ const merged = ctx.codec.merge(stored, observation);
35
+ if (!ctx.codec.validate(merged))
36
+ throw refusal('pull', id, 'invalid-quota', 'the quota codec rejected the merged map');
37
+ return merged;
38
+ }
6
39
  /**
7
40
  * Merges a quota observation into a row's stored map under the store locks,
8
41
  * after attribution passes. Needs no row lock, so it is permitted from
@@ -13,37 +46,18 @@ export async function recordQuota(rt, id, attribution, observation) {
13
46
  const locks = new LockStack(ctx.lockDefaults, ctx.lockEnv);
14
47
  const progress = { writes: 0 };
15
48
  try {
16
- if (!isCredentialEpoch(attribution?.credentialEpoch))
17
- throw refusal('pull', id, 'invalid-input', 'the credential epoch the reading was issued for must be a positive safe integer');
49
+ assertQuotaEpoch(id, attribution);
18
50
  await withTransaction(ctx, locks, progress, { operation: 'pull', rowId: id }, async (tx) => {
19
51
  tx.assertNotStaged(id);
20
52
  const row = tx.row(id);
21
- if (!row)
22
- throw unknownRow('pull', id);
23
- if (row.invalid)
24
- throw refusal('pull', id, 'invalid-row', `row ${id} is invalid`);
25
53
  // A reading belongs to one (epoch, identity, credential). A row
26
54
  // holding no credential, or torn between the writes of a replace,
27
55
  // has no such triple on disk, so no reading is recorded for it.
28
- if (!row.credential)
29
- throw refusal('pull', id, 'no-credential', `row ${id} holds no credential`);
30
- requireBound('pull', row);
56
+ assertQuotaRow(id, attribution, row);
31
57
  const entry = tx.entry(id);
32
- if (row.torn ||
33
- !entry ||
34
- entry.credentialEpoch !== attribution.credentialEpoch ||
35
- row.identity !== attribution.identity)
36
- throw new PoolOperationError({
37
- operation: 'pull',
38
- rowId: id,
39
- phase: 'pull',
40
- retryable: true,
41
- kind: 'attribution',
42
- message: `quota for ${id} was issued for a credential the row no longer holds`,
43
- });
44
- const merged = ctx.codec.merge(entry.quota, observation);
45
- if (!ctx.codec.validate(merged))
46
- throw refusal('pull', id, 'invalid-quota', 'the quota codec rejected the merged map');
58
+ if (!entry)
59
+ throw unknownRow('pull', id);
60
+ const merged = mergedQuota(ctx, id, entry.quota, observation);
47
61
  tx.setEntry(id, { ...entry, quota: merged, needsFirstReading: false });
48
62
  await tx.commitConfig();
49
63
  },
@@ -16,6 +16,19 @@ export interface OpenPoolStoreOptions {
16
16
  configPath: string;
17
17
  statePath: string;
18
18
  quota: QuotaCodec;
19
+ /**
20
+ * Opt in to detached, coalesced quota publication, with any `quota` codec.
21
+ * Each accepted observation is queued whole for its row and binding, and
22
+ * the queue is applied in arrival order through `quota.merge` under the
23
+ * store locks, so the codec's own merge decides which fields a newer or
24
+ * older reading replaces. Accepted readings appear in read/load
25
+ * immediately, merged the same way; the returned promise still rejects
26
+ * current-row refusals before queueing. Await dispose or close at shutdown
27
+ * to drain.
28
+ */
29
+ coalesceQuota?: boolean;
30
+ /** Bounded quota drain on close/dispose, in milliseconds; defaults to 1,000. */
31
+ quotaDrainTimeoutMs?: number;
19
32
  /**
20
33
  * The codec of the provider state kept beside each row's credential (see
21
34
  * `ProviderStateCodec`). Without it no row shows a provider state, and
@@ -189,6 +202,10 @@ export interface PoolStore {
189
202
  }>;
190
203
  refresh(id: string, provider: ProviderRefresh, options?: RefreshOptions): Promise<RefreshOutcome>;
191
204
  recordQuota(id: string, attribution: Attribution, observation: unknown): Promise<void>;
205
+ /** Drain opted-in quota publication; subsequent coalesced readings refuse. Idempotent. */
206
+ close(): Promise<void>;
207
+ /** Same bounded quota drain as close(). Does not wait for provider pull hooks. */
208
+ dispose(): Promise<void>;
192
209
  /** Fires a pull for a row admission refused for want of a reading. */
193
210
  requestReading(id: string): void;
194
211
  /** Resolves once every pull fired so far has settled. */
@@ -3,6 +3,7 @@ import { initializePool, readPool, } from './mutate.js';
3
3
  import { updateProviderStateRow, } from './provider-state.js';
4
4
  import { publication, publishRoster, } from './publication.js';
5
5
  import { PullScheduler } from './pull.js';
6
+ import { PoolQuotaWriter } from './quota-writer.js';
6
7
  import { refreshRow, } from './refresh.js';
7
8
  import { POOL_LOCK_DEFAULTS, } from './refresh-lock.js';
8
9
  import { addRow, disableRow, enableRow, recordRowIdentity, removeRow, reorderRows, replaceRow, rotateRow, } from './rows.js';
@@ -80,9 +81,21 @@ export function openPoolStore(options) {
80
81
  rowLockOptions: options.rowLockOptions ?? {},
81
82
  firePull: (id, reason) => pulls.fire(id, reason),
82
83
  };
84
+ const quotaWriter = options.coalesceQuota
85
+ ? new PoolQuotaWriter(rt, options.quotaDrainTimeoutMs)
86
+ : undefined;
87
+ const record = (id, attribution, observation) => quotaWriter
88
+ ? quotaWriter.record(id, attribution, observation)
89
+ : recordQuota(rt, id, attribution, observation);
90
+ rt.publishQuota = record;
91
+ const overlay = (result) => {
92
+ if (result.status === 'ready' && quotaWriter)
93
+ return { ...result, rows: quotaWriter.overlay(result.rows) };
94
+ return result;
95
+ };
83
96
  return {
84
97
  async load() {
85
- const result = await readPool(ctx);
98
+ const result = overlay(await readPool(ctx));
86
99
  if (result.status === 'ready') {
87
100
  // A torn row is no candidate, but its pull is fired too: the pull
88
101
  // completes the row on disk before reading it, so an interrupted
@@ -96,7 +109,7 @@ export function openPoolStore(options) {
96
109
  return toLoad(result);
97
110
  },
98
111
  async read() {
99
- return toLoad(await readPool(ctx));
112
+ return toLoad(overlay(await readPool(ctx)));
100
113
  },
101
114
  async initialize(input = {}) {
102
115
  return { status: await initializePool(ctx, input.dropKeys ?? []) };
@@ -115,7 +128,9 @@ export function openPoolStore(options) {
115
128
  updateSettings: (mutator, callOptions) => updatePoolSettings(rt, mutator, callOptions),
116
129
  recordIdentity: (id, identity, attribution, callOptions) => recordRowIdentity(rt, id, identity, attribution, callOptions),
117
130
  refresh: (id, provider, callOptions) => refreshRow(rt, id, provider, callOptions),
118
- recordQuota: (id, attribution, observation) => recordQuota(rt, id, attribution, observation),
131
+ recordQuota: record,
132
+ close: () => quotaWriter?.close() ?? Promise.resolve(),
133
+ dispose: () => quotaWriter?.close() ?? Promise.resolve(),
119
134
  requestReading: (id) => pulls.fire(id, 'admission'),
120
135
  pullsSettled: () => pulls.settled(),
121
136
  };
@@ -105,7 +105,9 @@ export class PullScheduler {
105
105
  cause,
106
106
  });
107
107
  }
108
- await recordQuota(rt, id, {
108
+ const publish = rt.publishQuota ??
109
+ ((rowId, attribution, value) => recordQuota(rt, rowId, attribution, value));
110
+ await publish(id, {
109
111
  credentialEpoch: request.credentialEpoch,
110
112
  ...(request.identity !== undefined
111
113
  ? { identity: request.identity }