dsh-keys-manager 0.2.2 → 0.2.3

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/lib/index.d.ts CHANGED
@@ -240,12 +240,15 @@ export declare class KeysManagerService {
240
240
  * order is preserved so the older observation can never commit after the
241
241
  * newer one and leave the row on a stale generation.
242
242
  *
243
- * Round-8 finding I-000106: activation joins the same chain. The only
244
- * writer of `row.oauth.grantRef` is {@link reconcileGrantRecord}, which runs
245
- * outside the mutation queue, so activation's record read and its projection
246
- * write must be mutually exclusive with that writer — otherwise a rotation
247
- * landing between them leaves the projection on a generation the row no
248
- * longer names.
243
+ * Round-8 finding I-000106 assumed activation would join this chain. It does
244
+ * not: activation runs its compare-and-write inside the mutation queue turn,
245
+ * and the only writer of `row.oauth.grantRef` is {@link reconcileGrantRecord},
246
+ * which runs outside that queue. The fence is therefore a generation re-check
247
+ * on both sides of the write — before it (the row must still name the
248
+ * generation that was read) and after it (a rotation commit can land inside
249
+ * the seam's compare-and-write await, which only reads the projection key).
250
+ * Chain members are the echo reconciler, the key-test borrow and adoption
251
+ * classification.
249
252
  */
250
253
  private readonly grantReconciliations;
251
254
  /**
@@ -340,6 +343,20 @@ export declare class KeysManagerService {
340
343
  private credentialsMounted;
341
344
  /** Providers whose bounded-wait warning already fired (warn once). */
342
345
  private readonly hydrationWaitWarned;
346
+ /**
347
+ * Stream attempts each provider served without an attributed account
348
+ * (`I-000120`, D8): the ownership read had not landed, or it had failed.
349
+ * Counted at the one managed serving site (`wrapStream`), never in the
350
+ * hydration pass — a failed read serves no request, it only makes later
351
+ * attempts unattributed. Two earlier branches are outside this definition and
352
+ * stay uncounted: a provider this manager does not manage (no ownership is
353
+ * ever asked for) and the `mismatch` profile shape, which hands the request
354
+ * to pi-ai untouched precisely because this manager's own binding cannot
355
+ * describe what serves it. Cleared only when a later read decides the route's
356
+ * attribution; process-local, never persisted and never on the warnings
357
+ * channel.
358
+ */
359
+ private readonly unattributedRequests;
343
360
  /** Channels whose lost-subscription warning already fired (warn once). */
344
361
  private readonly subscriptionLostWarnings;
345
362
  /** Disposer for the `authorization/settled` subscription. */
@@ -470,6 +487,14 @@ export declare class KeysManagerService {
470
487
  * previous grant's.
471
488
  */
472
489
  private noteProjectionCommitted;
490
+ /**
491
+ * Whether a claim landed after `generation` was captured. A reconciliation
492
+ * reads its snapshot before its awaited record read and decides after it, so
493
+ * an activation (or login) that claimed the route in between owns the answer
494
+ * now: the stale body must not clear the claim, mark the route foreign, or
495
+ * re-point the binding. The hydration write uses the same guard.
496
+ */
497
+ private ownershipSuperseded;
473
498
  /**
474
499
  * Claim the projection owner for one account row, bumping the provider's
475
500
  * ownership generation so an in-flight hydration read of an older state
@@ -959,24 +984,79 @@ export declare class KeysManagerService {
959
984
  * Make one account's grant payload the route's stored credential
960
985
  * (D1 activation projection).
961
986
  *
962
- * Round-8 finding I-000106 (must-fix): the decide snapshot used to be read
963
- * before the seam's lock, and `reconcileGrantRecord` runs OUTSIDE the
964
- * mutation queue — so a rotation echo could repoint `row.oauth.grantRef` and
965
- * delete the record being projected between the read and the write, leaving
966
- * the route on an older generation than the row names. The whole
967
- * read → project → claim section therefore runs on the provider's grant
968
- * chain ({@link scheduleGrantReconciliation}) — the same chain the
969
- * reconciler bodies use. A rotation committed before this body is observed
970
- * in `row.oauth.grantRef`; one that arrives while the body runs is queued
971
- * behind it and reconciles the generation this body projected. Either way
972
- * the projection holds exactly the generation the row names.
973
- *
974
- * The seam's per-key lock still serializes the read-decide-replace steps of
975
- * `modifyRecord` (credentials/src/index.ts:236-250) against other writers.
976
- * The account whose payload lands becomes the pool's active account and is
977
- * cascade-selectable again.
987
+ * Plan iter-pool-activate-queue, "Spike outcome" — the contract this method
988
+ * implements. The slow half (read the row's grant, validate it, capture what
989
+ * the route serves now) runs outside the mutation queue, so a slow credential
990
+ * store does not delay unrelated mutations. The mutation queue turn owns every
991
+ * write:
992
+ *
993
+ * 1. admission checks in queue order, which produce this method's error
994
+ * surface (`Provider "…" is not managed`, `Account "…" not found on …`,
995
+ * `Key "…" is not an OAuth account`, `The credential store does not support
996
+ * records`) — plus a re-check that the row still names the generation the
997
+ * read used;
998
+ * 2. the projection write as a compare-and-write under the seam's per-key
999
+ * lock (credentials/src/index.ts:236-250): it lands only while the live
1000
+ * record still equals the captured baseline, "both absent" included. A
1001
+ * rotation, a removal or any foreign writer that replaced the projection
1002
+ * meanwhile makes the compare fail, so this call refuses with the retryable
1003
+ * `could not be projected` message and leaves the newer projection
1004
+ * untouched — there is nothing to delete, because nothing was written;
1005
+ * 3. the owner claim, the row writes and the returned `buildState`.
1006
+ *
1007
+ * There is deliberately no shutdown check after the write: `close()` awaits
1008
+ * this queue before it closes the store, so a turn that started in time always
1009
+ * finishes (measured in the spike that produced this contract). A check there
1010
+ * is what used to leave an orphan projection behind.
1011
+ *
1012
+ * The one write the turn cannot make durable by itself is a rejected state
1013
+ * flush; see {@link compensateActivationFlush} for that compensation.
978
1014
  */
979
1015
  activateAccount(provider: string, accountId: string): Promise<StateView>;
1016
+ /**
1017
+ * The slow half of an activation, outside the queue: read the row's grant
1018
+ * generation, validate it, and capture the projection the route serves now.
1019
+ */
1020
+ private prepareActivation;
1021
+ /** Whether the projection still holds the value this activation read. */
1022
+ private matchesActivationBaseline;
1023
+ /**
1024
+ * Write the projection only while the route still serves the baseline. The
1025
+ * seam's `modifyRecord` mutate body runs under the per-key lock, so this is
1026
+ * the one atomic primitive the activation has (I-000090: `deleteRecord` has
1027
+ * no expected-payload form). Returns the payload written, or `undefined` when
1028
+ * the compare failed and nothing was touched.
1029
+ */
1030
+ private compareAndWriteProjection;
1031
+ /**
1032
+ * Undo one activation whose state flush was rejected: the row this turn
1033
+ * cleared in memory, the owner claim it took, and the projection it wrote
1034
+ * (back to the captured baseline).
1035
+ *
1036
+ * The baseline decides the mechanism. A present baseline is restored with the
1037
+ * same compare-and-write the projection used; only an absent baseline has to
1038
+ * remove a record, and only while that record is still this activation's own
1039
+ * payload. A foreign writer's record is never deleted.
1040
+ */
1041
+ private compensateActivationFlush;
1042
+ /** Put the route's owner classification back to a captured snapshot. */
1043
+ private applyActivationClassification;
1044
+ /** Put the projection back to the present baseline, if it is still ours. */
1045
+ private restoreActivationProjection;
1046
+ /**
1047
+ * The delete authority's proof for an activation's own projection, as an
1048
+ * explicit three-valued answer (QC R2): only `matches` may authorize touching
1049
+ * the record. `unknown` is a store failure — a closed seam, an unreadable
1050
+ * record — and proves nothing, so it is never permission.
1051
+ */
1052
+ private activationProjectionProof;
1053
+ /**
1054
+ * Delete one projection an activation just proved it still owns, through the
1055
+ * same guarded shape the demotion path uses (MF2): the intentional-removal
1056
+ * marker is armed before the delete, so the deletion's own echo is read as
1057
+ * ours instead of as an out-of-band removal.
1058
+ */
1059
+ private deleteActivationProjection;
980
1060
  /**
981
1061
  * Remote: send one minimal request with the selected key or account row.
982
1062
  *
@@ -1344,6 +1424,12 @@ export declare class KeysManagerService {
1344
1424
  private mutateSettings;
1345
1425
  /** Prune expired bindings at most once a minute, without a restart. */
1346
1426
  private pruneBindings;
1427
+ /**
1428
+ * Project one provider's unattributed-request count into its row. Additive
1429
+ * and optional (`I-000120`): a row with no count spreads nothing, so an
1430
+ * older snapshot and a healthy store render exactly as before.
1431
+ */
1432
+ private unattributedView;
1347
1433
  /** Build the complete UI state. */
1348
1434
  buildState(): StateView;
1349
1435
  /**