@kubun/plugin-p2p 0.17.2 → 0.17.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.
@@ -1389,14 +1389,14 @@ export function createPeerContext(ctx, deps) {
1389
1389
  },
1390
1390
  beginProvisioningExpectation: async (params)=>{
1391
1391
  const store = await getCredentialStore(deps.stores);
1392
- const attemptId = crypto.randomUUID();
1392
+ const attemptID = params.attemptID ?? deps.runtime.getRandomID();
1393
1393
  await store.beginProvisioningAttempt({
1394
1394
  ownerDID: normalizeDID(params.ownerDID),
1395
1395
  grantorDID: normalizeDID(params.grantorDID),
1396
- attemptId
1396
+ attemptID
1397
1397
  });
1398
1398
  return {
1399
- attemptId
1399
+ attemptID
1400
1400
  };
1401
1401
  },
1402
1402
  recordProvisioningExpectation: async (params)=>{
@@ -1422,7 +1422,7 @@ export function createPeerContext(ctx, deps) {
1422
1422
  return await txStore.recordProvisioningAttempt({
1423
1423
  ownerDID: owner,
1424
1424
  grantorDID: grantor,
1425
- attemptId: params.attemptId,
1425
+ attemptID: params.attemptID,
1426
1426
  epoch: params.epoch,
1427
1427
  heldEpoch
1428
1428
  });
@@ -1433,7 +1433,14 @@ export function createPeerContext(ctx, deps) {
1433
1433
  await store.abandonProvisioningAttempt({
1434
1434
  ownerDID: normalizeDID(params.ownerDID),
1435
1435
  grantorDID: normalizeDID(params.grantorDID),
1436
- attemptId: params.attemptId
1436
+ attemptID: params.attemptID
1437
+ });
1438
+ },
1439
+ abandonProvisioningExpectations: async (params)=>{
1440
+ const store = await getCredentialStore(deps.stores);
1441
+ await store.abandonProvisioningAttempts({
1442
+ ownerDID: normalizeDID(params.ownerDID),
1443
+ grantorDID: normalizeDID(params.grantorDID)
1437
1444
  });
1438
1445
  },
1439
1446
  provisioningHeldEpoch: async (params)=>{
@@ -7,10 +7,25 @@ import { type DelegatedWrappingCheckDeps } from './grantor-authority.js';
7
7
  * decrypt-materialization; `reason` explains a `false` verdict (absent on
8
8
  * `complete`).
9
9
  */
10
+ export type CredentialProvisioningGrantorStatus = {
11
+ grantorDID: string;
12
+ /** Manifest at/above floor, proof verifies now, and every key materialized. */
13
+ satisfied: boolean;
14
+ /**
15
+ * The held delegation proof verifies right now. `false` only when the
16
+ * administer-chain check on a HELD manifest returns false (revoked/expired
17
+ * OR transiently unvalidatable). An un-pulled/below-floor manifest and a
18
+ * self-grantor leave it `true` — no held proof has failed.
19
+ */
20
+ authorityValid: boolean;
21
+ /** This grantor's keys not yet materialized here. */
22
+ missing: Array<string>;
23
+ };
10
24
  export type CredentialProvisioningReadiness = {
11
25
  complete: boolean;
12
26
  missing: Array<string>;
13
27
  reason?: 'not-initiated' | 'pending' | 'incomplete';
28
+ grantors: Array<CredentialProvisioningGrantorStatus>;
14
29
  };
15
30
  /**
16
31
  * Whether one expected key is materialized on this device AT the manifest's
@@ -76,7 +76,8 @@ import { checkAdministerChain } from './grantor-authority.js';
76
76
  return {
77
77
  complete: false,
78
78
  missing: [],
79
- reason: 'pending'
79
+ reason: 'pending',
80
+ grantors: []
80
81
  };
81
82
  }
82
83
  // Rule 2: nothing has ever been asked for — never a vacuous complete.
@@ -84,7 +85,8 @@ import { checkAdministerChain } from './grantor-authority.js';
84
85
  return {
85
86
  complete: false,
86
87
  missing: [],
87
- reason: 'not-initiated'
88
+ reason: 'not-initiated',
89
+ grantors: []
88
90
  };
89
91
  }
90
92
  const manifests = await store.listManifests({
@@ -96,13 +98,21 @@ import { checkAdministerChain } from './grantor-authority.js';
96
98
  manifest
97
99
  ]));
98
100
  const missing = [];
101
+ const grantors = [];
99
102
  let allSatisfied = true;
100
103
  for (const expectation of expectations){
101
104
  const grantor = normalizeDID(expectation.grantorDID);
102
105
  const manifest = byGrantor.get(grantor);
103
106
  // No held manifest at/above the floor: this expectation names no known
104
- // target keys to report as `missing` — it is simply unsatisfied.
107
+ // target keys to report as `missing` — it is simply unsatisfied. No held
108
+ // proof has failed here, so `authorityValid` stays true.
105
109
  if (manifest == null || manifest.record.epoch < expectation.epochFloor) {
110
+ grantors.push({
111
+ grantorDID: grantor,
112
+ satisfied: false,
113
+ authorityValid: true,
114
+ missing: []
115
+ });
106
116
  allSatisfied = false;
107
117
  continue;
108
118
  }
@@ -114,9 +124,16 @@ import { checkAdministerChain } from './grantor-authority.js';
114
124
  });
115
125
  if (!authorized) {
116
126
  // Expired/revoked/unwired proof: fail-closed means unmet, not thrown.
127
+ grantors.push({
128
+ grantorDID: grantor,
129
+ satisfied: false,
130
+ authorityValid: false,
131
+ missing: []
132
+ });
117
133
  allSatisfied = false;
118
134
  continue;
119
135
  }
136
+ const grantorMissing = [];
120
137
  for (const key of manifest.record.keys){
121
138
  let materialized = false;
122
139
  try {
@@ -134,21 +151,32 @@ import { checkAdministerChain } from './grantor-authority.js';
134
151
  materialized = false;
135
152
  }
136
153
  if (!materialized) {
137
- missing.push(key.keyID);
138
- allSatisfied = false;
154
+ grantorMissing.push(key.keyID);
139
155
  }
140
156
  }
157
+ if (grantorMissing.length > 0) {
158
+ missing.push(...grantorMissing);
159
+ allSatisfied = false;
160
+ }
161
+ grantors.push({
162
+ grantorDID: grantor,
163
+ satisfied: grantorMissing.length === 0,
164
+ authorityValid: true,
165
+ missing: grantorMissing
166
+ });
141
167
  }
142
168
  if (allSatisfied && missing.length === 0) {
143
169
  return {
144
170
  complete: true,
145
- missing: []
171
+ missing: [],
172
+ grantors
146
173
  };
147
174
  }
148
175
  return {
149
176
  complete: false,
150
177
  missing,
151
- reason: 'incomplete'
178
+ reason: 'incomplete',
179
+ grantors
152
180
  };
153
181
  }
154
182
  /**
package/lib/schema.js CHANGED
@@ -680,6 +680,17 @@ type GrantDeviceCredentialsPayload {
680
680
  epoch: Int!
681
681
  }
682
682
 
683
+ """One grantor's provisioning verdict within a credentialProvisioningStatus."""
684
+ type CredentialProvisioningGrantorStatus {
685
+ grantorDID: DID!
686
+ """Manifest at/above floor, proof verifies now, every key materialized."""
687
+ satisfied: Boolean!
688
+ """False only when a held manifest's administer chain fails now (revoked/expired or transiently unvalidatable); un-pulled or self-grantor stays true."""
689
+ authorityValid: Boolean!
690
+ """This grantor's keyIDs not yet materialized here."""
691
+ missing: [ID!]!
692
+ }
693
+
683
694
  """
684
695
  Readiness of this device's credential provisioning for one owner. A key counts
685
696
  materialized only when its wrapping decrypt-verifies (or, for a zero-entry key,
@@ -695,14 +706,13 @@ type CredentialProvisioningStatus {
695
706
  (asked for, not yet fully materialized/proven). Absent when complete.
696
707
  """
697
708
  reason: String
709
+ """Per-grantor breakdown; empty on pending/not-initiated."""
710
+ grantors: [CredentialProvisioningGrantorStatus!]!
698
711
  }
699
712
 
700
713
  type BeginProvisioningExpectationPayload {
701
- """
702
- Freshly minted attempt id; pass it to recordProvisioningExpectation /
703
- abandonProvisioningExpectation to close this attempt.
704
- """
705
- attemptId: ID!
714
+ """The effective attempt id (caller-supplied or minted); pass to record/abandon to close it."""
715
+ attemptID: ID!
706
716
  }
707
717
 
708
718
  enum RecordProvisioningExpectationStatus {
@@ -745,25 +755,32 @@ extend type Mutation {
745
755
  grantDeviceCredentials(groupID: ID!, recipientDID: DID!, ownerDID: DID!, minEpoch: Int): GrantDeviceCredentialsPayload!
746
756
  """
747
757
  Arm this device to converge a credential grant for ownerDID from grantorDID:
748
- write the provisioning-expectation row that lets the credential-reconcile PULL
749
- fetch that owner's buckets (a delegate-signed grant converges only via the
750
- pull, never the live apply). Call before asking the grantor to grant, then
751
- recordProvisioningExpectation once the grant returns its epoch. Mirrors the
752
- credentialProvisioningStatus query's recipient-local surface.
758
+ write the provisioning-expectation row the credential-reconcile pull fetches
759
+ against. Call before asking the grantor to grant, then recordProvisioningExpectation
760
+ once the grant returns its epoch. Optional attemptID: persist it before calling
761
+ for crash-recovery; omit to mint server-side. Idempotent only within the
762
+ outstanding window (a replay after record/abandon re-pins 'pending').
753
763
  """
754
- beginProvisioningExpectation(ownerDID: DID!, grantorDID: DID!): BeginProvisioningExpectationPayload!
764
+ beginProvisioningExpectation(ownerDID: DID!, grantorDID: DID!, attemptID: ID): BeginProvisioningExpectationPayload!
755
765
  """
756
766
  Bind the provisioning floor for an in-flight attempt to the epoch its grant was
757
767
  signed under. Returns RECORDED when the floor binds, or RETRY with the held
758
768
  epoch when a higher manifest already landed (re-grant with minEpoch, then record
759
769
  the new attempt).
760
770
  """
761
- recordProvisioningExpectation(ownerDID: DID!, grantorDID: DID!, attemptId: ID!, epoch: Int!): RecordProvisioningExpectationPayload!
771
+ recordProvisioningExpectation(ownerDID: DID!, grantorDID: DID!, attemptID: ID!, epoch: Int!): RecordProvisioningExpectationPayload!
762
772
  """
763
- Stop tracking one provisioning attempt. Removes only this attemptId; never binds
773
+ Stop tracking one provisioning attempt. Removes only this attemptID; never binds
764
774
  or lowers a floor. Returns true.
765
775
  """
766
- abandonProvisioningExpectation(ownerDID: DID!, grantorDID: DID!, attemptId: ID!): Boolean!
776
+ abandonProvisioningExpectation(ownerDID: DID!, grantorDID: DID!, attemptID: ID!): Boolean!
777
+ """
778
+ Clear EVERY outstanding provisioning attempt for a (owner, grantor) scope —
779
+ the recovery path when an attempt id was lost. Never binds or lowers a floor.
780
+ Exclusive recovery: also cancels any other in-flight attempt, so ensure none
781
+ is live. Returns true.
782
+ """
783
+ abandonProvisioningExpectations(ownerDID: DID!, grantorDID: DID!): Boolean!
767
784
  joinPeerGroup(peerDID: ID!, groupID: ID!): JoinPeerGroupPayload!
768
785
  """
769
786
  Apply one circle's desired sync end state. A null argument leaves that dimension
@@ -1099,14 +1116,17 @@ export function createP2PSchemaExtension(emitter, syncManager, logger = getKubun
1099
1116
  beginProvisioningExpectation: async (_source, args, context)=>{
1100
1117
  return await requireP2P(context).peer.beginProvisioningExpectation({
1101
1118
  ownerDID: args.ownerDID,
1102
- grantorDID: args.grantorDID
1119
+ grantorDID: args.grantorDID,
1120
+ ...args.attemptID == null ? {} : {
1121
+ attemptID: args.attemptID
1122
+ }
1103
1123
  });
1104
1124
  },
1105
1125
  recordProvisioningExpectation: async (_source, args, context)=>{
1106
1126
  const result = await requireP2P(context).peer.recordProvisioningExpectation({
1107
1127
  ownerDID: args.ownerDID,
1108
1128
  grantorDID: args.grantorDID,
1109
- attemptId: args.attemptId,
1129
+ attemptID: args.attemptID,
1110
1130
  epoch: args.epoch
1111
1131
  });
1112
1132
  return {
@@ -1118,7 +1138,14 @@ export function createP2PSchemaExtension(emitter, syncManager, logger = getKubun
1118
1138
  await requireP2P(context).peer.abandonProvisioningExpectation({
1119
1139
  ownerDID: args.ownerDID,
1120
1140
  grantorDID: args.grantorDID,
1121
- attemptId: args.attemptId
1141
+ attemptID: args.attemptID
1142
+ });
1143
+ return true;
1144
+ },
1145
+ abandonProvisioningExpectations: async (_source, args, context)=>{
1146
+ await requireP2P(context).peer.abandonProvisioningExpectations({
1147
+ ownerDID: args.ownerDID,
1148
+ grantorDID: args.grantorDID
1122
1149
  });
1123
1150
  return true;
1124
1151
  },
package/lib/types.d.ts CHANGED
@@ -639,6 +639,13 @@ export type CredentialProvisioningStatusParams = {
639
639
  */
640
640
  ownerDID: string;
641
641
  };
642
+ export type CredentialProvisioningGrantorStatus = {
643
+ grantorDID: string;
644
+ satisfied: boolean;
645
+ /** Held proof verifies now; `false` only when a held manifest's proof fails. */
646
+ authorityValid: boolean;
647
+ missing: Array<string>;
648
+ };
642
649
  export type CredentialProvisioningStatusData = {
643
650
  /** True iff every durable expectation is satisfied — `missing` is empty. */
644
651
  complete: boolean;
@@ -653,22 +660,41 @@ export type CredentialProvisioningStatusData = {
653
660
  * `'incomplete'` (recorded but not yet satisfied). Absent when `complete`.
654
661
  */
655
662
  reason?: 'not-initiated' | 'pending' | 'incomplete';
663
+ /**
664
+ * Per-grantor breakdown, populated on the post-checks path (empty on
665
+ * `pending`/`not-initiated`). Lets a driver tell an un-pulled manifest
666
+ * (`authorityValid:true`, keep pulling) from a held proof that fails to
667
+ * verify (`authorityValid:false`, back off and await a fresh grant).
668
+ */
669
+ grantors: Array<CredentialProvisioningGrantorStatus>;
656
670
  };
657
671
  export type BeginProvisioningExpectationParams = {
658
672
  /** The credential owner this expectation tracks provisioning for. */
659
673
  ownerDID: string;
660
674
  /** The grantor this device expects a manifest from. */
661
675
  grantorDID: string;
676
+ /**
677
+ * Optional caller-supplied attempt id. Supply and persist it BEFORE calling
678
+ * to make begin crash-recoverable — after a crash the caller still knows the
679
+ * id to abandon. Idempotent only within the outstanding window: a replay
680
+ * after record/abandon re-creates the attempt and re-pins `pending`. Omit to
681
+ * have the id minted server-side.
682
+ */
683
+ attemptID?: string;
662
684
  };
663
685
  export type BeginProvisioningExpectationData = {
664
- /** Freshly minted attempt id; pass it to `recordProvisioningExpectation`/`abandonProvisioningExpectation`. */
665
- attemptId: string;
686
+ /**
687
+ * The effective attempt id for this attempt: the caller-supplied
688
+ * `attemptID` when one was given, else a freshly minted one. Pass it to
689
+ * `recordProvisioningExpectation`/`abandonProvisioningExpectation`.
690
+ */
691
+ attemptID: string;
666
692
  };
667
693
  export type RecordProvisioningExpectationParams = {
668
694
  ownerDID: string;
669
695
  grantorDID: string;
670
696
  /** The id `beginProvisioningExpectation` minted for this attempt. */
671
- attemptId: string;
697
+ attemptID: string;
672
698
  /** The epoch this attempt's grant call was allocated and signed under. */
673
699
  epoch: number;
674
700
  };
@@ -683,7 +709,11 @@ export type AbandonProvisioningExpectationParams = {
683
709
  ownerDID: string;
684
710
  grantorDID: string;
685
711
  /** The id to stop tracking. Removes ONLY this id; never binds or lowers a floor. */
686
- attemptId: string;
712
+ attemptID: string;
713
+ };
714
+ export type AbandonProvisioningExpectationsParams = {
715
+ ownerDID: string;
716
+ grantorDID: string;
687
717
  };
688
718
  export type ProvisioningHeldEpochParams = {
689
719
  ownerDID: string;
@@ -867,7 +897,7 @@ export type PeerRequestContext = {
867
897
  credentialProvisioningStatus: (params: CredentialProvisioningStatusParams) => Promise<CredentialProvisioningStatusData>;
868
898
  /**
869
899
  * Begin tracking a provisioning attempt for `(ownerDID, grantorDID)`: mints
870
- * an `attemptId` and forces `credentialProvisioningStatus` to `pending`
900
+ * an `attemptID` and forces `credentialProvisioningStatus` to `pending`
871
901
  * until recorded/abandoned. Call BEFORE asking the grantor to grant, to
872
902
  * close the epoch-allocation race window.
873
903
  */
@@ -878,14 +908,23 @@ export type PeerRequestContext = {
878
908
  * attempt's epoch still exceeds it, the floor advances (`'recorded'`);
879
909
  * otherwise a higher manifest already arrived and the attempt stays
880
910
  * outstanding (`'retry'`, `minEpoch`) — retry the grant and call again with
881
- * the same `attemptId`.
911
+ * the same `attemptID`.
882
912
  */
883
913
  recordProvisioningExpectation: (params: RecordProvisioningExpectationParams) => Promise<RecordProvisioningExpectationData>;
884
914
  /**
885
915
  * Stop tracking a dead/abandoned provisioning attempt: removes only this
886
- * `attemptId` from the outstanding set, never touches the epoch floor.
916
+ * `attemptID` from the outstanding set, never touches the epoch floor.
887
917
  */
888
918
  abandonProvisioningExpectation: (params: AbandonProvisioningExpectationParams) => Promise<void>;
919
+ /**
920
+ * Clear EVERY outstanding provisioning attempt for a (owner, grantor) scope
921
+ * without an id — the recovery path for an orphaned attempt whose id was
922
+ * lost. Never touches the epoch floor. Exclusive-recovery contract: also
923
+ * cancels any other live coordinator's attempt for the scope, so the caller
924
+ * must ensure none is in flight; when the id is known, prefer
925
+ * `abandonProvisioningExpectation`.
926
+ */
927
+ abandonProvisioningExpectations: (params: AbandonProvisioningExpectationsParams) => Promise<void>;
889
928
  /**
890
929
  * This device's currently-held epoch for `(ownerDID, grantorDID)` — max
891
930
  * epoch among held manifests from that grantor, or `0`. Feeds `minEpoch`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubun/plugin-p2p",
3
- "version": "0.17.2",
3
+ "version": "0.17.3",
4
4
  "license": "see LICENSE.md",
5
5
  "sideEffects": false,
6
6
  "type": "module",
@@ -27,7 +27,7 @@
27
27
  "@kubun/db": "^0.17.0",
28
28
  "@kubun/db-adapter": "^0.17.0",
29
29
  "@kubun/engine": "^0.17.0",
30
- "@kubun/graphql": "^0.17.0",
30
+ "@kubun/graphql": "^0.17.1",
31
31
  "@kubun/hlc": "^0.17.0",
32
32
  "@kubun/http-util": "^0.17.0",
33
33
  "@kubun/id": "^0.17.0",
@@ -42,7 +42,7 @@
42
42
  "@kubun/protocol": "^0.17.0",
43
43
  "@kubun/store-blob": "^0.17.0",
44
44
  "@kubun/store-controller": "^0.17.0",
45
- "@kubun/store-credential": "^0.17.0",
45
+ "@kubun/store-credential": "^0.17.1",
46
46
  "@kubun/store-delegation": "^0.17.0",
47
47
  "@kubun/store-graph": "^0.17.0",
48
48
  "@kubun/store-p2p": "^0.17.0",
@@ -62,7 +62,7 @@
62
62
  "@sozai/runtime": "^0.1.0",
63
63
  "@sozai/schema": "^0.1.2",
64
64
  "@sozai/stream": "^0.2.0",
65
- "graphql": "^16.14.2",
65
+ "graphql": "^17.0.2",
66
66
  "kysely": "^0.29.5",
67
67
  "ts-mls": "2.0.0-rc.13"
68
68
  },