@koed-labs/server 0.0.0-stage → 0.10.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/LICENSE +201 -0
- package/README.md +64 -2
- package/bin/koed.js +7 -0
- package/package.json +26 -4
- package/third-party-notices.json +92 -0
- package/vendor/chunk-2BWTLF7A.js +13883 -0
- package/vendor/chunk-2DA7EFZV.js +924 -0
- package/vendor/chunk-2S3OGPUC.js +58 -0
- package/vendor/chunk-473OA62O.js +26827 -0
- package/vendor/chunk-6JRWGOQG.js +236 -0
- package/vendor/chunk-7NOQSX4T.js +14 -0
- package/vendor/chunk-7NXKPDF3.js +1586 -0
- package/vendor/chunk-A4NKL2PK.js +153 -0
- package/vendor/chunk-A6WMU2GG.js +48 -0
- package/vendor/chunk-AFLKCQHF.js +73 -0
- package/vendor/chunk-BTS4ZTF3.js +43 -0
- package/vendor/chunk-C3N4N5HR.js +108 -0
- package/vendor/chunk-DXTAWHY3.js +25 -0
- package/vendor/chunk-G4IIVVJO.js +56 -0
- package/vendor/chunk-GELDEJJ2.js +4337 -0
- package/vendor/chunk-HFM6ROJL.js +188 -0
- package/vendor/chunk-HK2H3MVE.js +328 -0
- package/vendor/chunk-HZJK5RWC.js +5088 -0
- package/vendor/chunk-J2L726BC.js +2497 -0
- package/vendor/chunk-JBGD4JLR.js +3779 -0
- package/vendor/chunk-JJNLH5GA.js +2470 -0
- package/vendor/chunk-JKOG52PE.js +128 -0
- package/vendor/chunk-K2EIFEQF.js +89 -0
- package/vendor/chunk-KUIWH33N.js +16 -0
- package/vendor/chunk-NEWIOOM6.js +34 -0
- package/vendor/chunk-NM5NXCVM.js +2404 -0
- package/vendor/chunk-PT3AUPGE.js +459 -0
- package/vendor/chunk-PY5LYLV7.js +143 -0
- package/vendor/chunk-QIIKMG3M.js +103 -0
- package/vendor/chunk-QT24SAW6.js +110 -0
- package/vendor/chunk-RVYJKKWA.js +22 -0
- package/vendor/chunk-SKVYP7NY.js +1647 -0
- package/vendor/chunk-TSRPHKAC.js +780 -0
- package/vendor/chunk-UI5RLBKQ.js +30 -0
- package/vendor/chunk-VPC5NLZG.js +259 -0
- package/vendor/chunk-W2HNI6AA.js +75 -0
- package/vendor/chunk-X6M525ZJ.js +20 -0
- package/vendor/chunk-XIZVLPHZ.js +18 -0
- package/vendor/chunk-XNFVSVBS.js +242 -0
- package/vendor/chunk-YCDEMYY6.js +2076 -0
- package/vendor/chunk-YJ6XUZT6.js +292 -0
- package/vendor/chunk-YJWREXN6.js +26020 -0
- package/vendor/chunk-YK4F4VY2.js +173 -0
- package/vendor/chunk-YOFI4PCH.js +36 -0
- package/vendor/chunk-ZUGNCQZE.js +247 -0
- package/vendor/chunk-ZXX4ZNJY.js +21 -0
- package/vendor/cli.js +7 -0
- package/vendor/component-manifest-catalog.js +8 -0
- package/vendor/control-plane.js +23 -0
- package/vendor/package.json +1 -0
- package/vendor/privacy-service-bootstrap.js +6 -0
|
@@ -0,0 +1,1647 @@
|
|
|
1
|
+
import { createRequire as createKoedRequire } from "node:module"; const require = createKoedRequire(import.meta.url);
|
|
2
|
+
import {
|
|
3
|
+
beginUpstreamDisconnectCleanup,
|
|
4
|
+
clearUpstreamBackendCapabilities,
|
|
5
|
+
collectUpstreamRegistryStatus,
|
|
6
|
+
createSecureUpstreamFetch,
|
|
7
|
+
getActiveUpstreamBackend,
|
|
8
|
+
registeredPrivateNetworkPolicy,
|
|
9
|
+
setActiveUpstreamBackend,
|
|
10
|
+
updateUpstreamBackendCredential,
|
|
11
|
+
updateUpstreamBackendRoutePolicy,
|
|
12
|
+
updateUpstreamDisconnectCleanup,
|
|
13
|
+
upstreamBackendAdvertisesCapability,
|
|
14
|
+
upstreamBackendSupportsTeamMemoryAnswerTasks,
|
|
15
|
+
upstreamDisconnectCleanupPending
|
|
16
|
+
} from "./chunk-YJWREXN6.js";
|
|
17
|
+
import {
|
|
18
|
+
ensureDeviceIdentity
|
|
19
|
+
} from "./chunk-XNFVSVBS.js";
|
|
20
|
+
import {
|
|
21
|
+
require_proper_lockfile
|
|
22
|
+
} from "./chunk-7NXKPDF3.js";
|
|
23
|
+
import {
|
|
24
|
+
assertSecureHttpTransport,
|
|
25
|
+
deleteLocalEdgeClientCredential,
|
|
26
|
+
deleteUpstreamCredentialSecret,
|
|
27
|
+
readLocalEdgeUpstreamEnrollmentBinding,
|
|
28
|
+
readUpstreamCredentialAuthorization,
|
|
29
|
+
storeEnrollmentCredentialCustody,
|
|
30
|
+
storeLocalEdgeClientCredential,
|
|
31
|
+
storeUpstreamCredentialSecret,
|
|
32
|
+
upstreamCredentialReferenceFor
|
|
33
|
+
} from "./chunk-473OA62O.js";
|
|
34
|
+
import {
|
|
35
|
+
__toESM
|
|
36
|
+
} from "./chunk-BTS4ZTF3.js";
|
|
37
|
+
|
|
38
|
+
// packages/koed-server/dist/upstream-enrollment.js
|
|
39
|
+
import { createHash as createHash2, randomBytes, randomUUID } from "node:crypto";
|
|
40
|
+
import { existsSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
41
|
+
import { mkdirSync as mkdirSync2 } from "node:fs";
|
|
42
|
+
import { dirname } from "node:path";
|
|
43
|
+
|
|
44
|
+
// packages/koed-server/dist/upstream-enrollment-lock.js
|
|
45
|
+
var import_proper_lockfile = __toESM(require_proper_lockfile(), 1);
|
|
46
|
+
import { createHash } from "node:crypto";
|
|
47
|
+
import { mkdirSync } from "node:fs";
|
|
48
|
+
import { resolve } from "node:path";
|
|
49
|
+
var LOCK_STALE_MS = 3e4;
|
|
50
|
+
var upstreamEnrollmentLockTarget = (paths, backendId) => {
|
|
51
|
+
const key = createHash("sha256").update(backendId).digest("hex").slice(0, 24);
|
|
52
|
+
return resolve(paths.runDir, `upstream-enrollment-${key}`);
|
|
53
|
+
};
|
|
54
|
+
var withUpstreamEnrollmentLock = async (paths, backendId, mutation) => {
|
|
55
|
+
mkdirSync(paths.runDir, { recursive: true, mode: 448 });
|
|
56
|
+
const release = await import_proper_lockfile.default.lock(upstreamEnrollmentLockTarget(paths, backendId), {
|
|
57
|
+
realpath: false,
|
|
58
|
+
stale: LOCK_STALE_MS,
|
|
59
|
+
update: LOCK_STALE_MS / 3,
|
|
60
|
+
retries: {
|
|
61
|
+
retries: 100,
|
|
62
|
+
factor: 1,
|
|
63
|
+
minTimeout: 25,
|
|
64
|
+
maxTimeout: 100,
|
|
65
|
+
randomize: true
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
try {
|
|
69
|
+
return await mutation();
|
|
70
|
+
} finally {
|
|
71
|
+
await release();
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// packages/koed-server/dist/upstream-enrollment-transaction.js
|
|
76
|
+
var executeUpstreamEnrollmentTransactionEffect = (decision2, handlers) => {
|
|
77
|
+
if (decision2.effect === "none")
|
|
78
|
+
return null;
|
|
79
|
+
const handler = handlers[decision2.effect];
|
|
80
|
+
if (!handler) {
|
|
81
|
+
throw new Error(`Enrollment transaction effect ${decision2.effect} has no handler.`);
|
|
82
|
+
}
|
|
83
|
+
return handler();
|
|
84
|
+
};
|
|
85
|
+
var decision = (current, update, effect = "none", temporary = false) => ({
|
|
86
|
+
next: { ...current, ...update },
|
|
87
|
+
effect,
|
|
88
|
+
temporary
|
|
89
|
+
});
|
|
90
|
+
var beginEffect = (current, phase, state, effect) => decision(current, { phase, state, pendingEffect: effect }, effect);
|
|
91
|
+
var createUpstreamEnrollmentTransaction = (input) => {
|
|
92
|
+
if (!input.id.trim())
|
|
93
|
+
throw new Error("Enrollment transaction id is required.");
|
|
94
|
+
if (!Number.isSafeInteger(input.generation) || input.generation < 1) {
|
|
95
|
+
throw new Error("Enrollment transaction generation must be positive.");
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
id: input.id,
|
|
99
|
+
generation: input.generation,
|
|
100
|
+
kind: input.kind,
|
|
101
|
+
phase: "prepared",
|
|
102
|
+
state: "pending",
|
|
103
|
+
pendingEffect: "stage_pending_custody"
|
|
104
|
+
};
|
|
105
|
+
};
|
|
106
|
+
var upstreamEnrollmentObservationApplies = (current, observation) => current.id === observation.transactionId && current.generation === observation.generation;
|
|
107
|
+
var decideUpstreamEnrollmentTransaction = (current, event) => {
|
|
108
|
+
if (event.type === "prepare") {
|
|
109
|
+
return current.phase === "prepared" ? decision(current, {}, "stage_pending_custody") : decision(current, {});
|
|
110
|
+
}
|
|
111
|
+
if (event.type === "effect_failed") {
|
|
112
|
+
return current.pendingEffect ? decision(current, { phase: "recovery_required" }) : decision(current, {});
|
|
113
|
+
}
|
|
114
|
+
if (event.type === "recover") {
|
|
115
|
+
if (!current.pendingEffect) {
|
|
116
|
+
return decision(current, {});
|
|
117
|
+
}
|
|
118
|
+
if (current.phase !== "recovery_required") {
|
|
119
|
+
return current.phase === "prepared" || current.phase === "awaiting_remote" || current.phase === "committing" || current.phase === "aborting" ? decision(current, {}, current.pendingEffect) : decision(current, {});
|
|
120
|
+
}
|
|
121
|
+
if (current.pendingEffect === "stage_pending_custody") {
|
|
122
|
+
return beginEffect(current, "aborting", "failed", "compensate_pending_custody");
|
|
123
|
+
}
|
|
124
|
+
const phase = current.pendingEffect === "commit_successor" ? "committing" : current.pendingEffect === "abort_pending" || current.pendingEffect === "revoke_active" ? "aborting" : "awaiting_remote";
|
|
125
|
+
return decision(current, { phase }, current.pendingEffect);
|
|
126
|
+
}
|
|
127
|
+
if (event.type === "effect_succeeded") {
|
|
128
|
+
if (current.phase === "committing") {
|
|
129
|
+
return decision(current, {
|
|
130
|
+
phase: "committed",
|
|
131
|
+
state: "exchanged",
|
|
132
|
+
pendingEffect: null
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
if (current.phase === "aborting") {
|
|
136
|
+
return decision(current, { phase: "aborted", pendingEffect: null });
|
|
137
|
+
}
|
|
138
|
+
if (current.phase === "prepared" && current.pendingEffect === "stage_pending_custody") {
|
|
139
|
+
return decision(current, {
|
|
140
|
+
phase: "awaiting_remote",
|
|
141
|
+
pendingEffect: null
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
if (current.phase === "awaiting_remote" && current.pendingEffect === "record_challenge") {
|
|
145
|
+
return decision(current, { pendingEffect: null });
|
|
146
|
+
}
|
|
147
|
+
return decision(current, {});
|
|
148
|
+
}
|
|
149
|
+
if (event.type === "challenge_created") {
|
|
150
|
+
return current.phase === "prepared" || current.phase === "awaiting_remote" ? decision(current, {
|
|
151
|
+
phase: "awaiting_remote",
|
|
152
|
+
state: "pending",
|
|
153
|
+
pendingEffect: "record_challenge"
|
|
154
|
+
}, "record_challenge") : decision(current, {});
|
|
155
|
+
}
|
|
156
|
+
if (event.type === "challenge_observed") {
|
|
157
|
+
if (event.status === "unknown")
|
|
158
|
+
return decision(current, {}, "none", true);
|
|
159
|
+
if (event.status === "pending") {
|
|
160
|
+
return decision(current, {
|
|
161
|
+
phase: "awaiting_remote",
|
|
162
|
+
state: "pending",
|
|
163
|
+
pendingEffect: null
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
if (event.status === "approved") {
|
|
167
|
+
return decision(current, {
|
|
168
|
+
phase: "awaiting_exchange",
|
|
169
|
+
state: "approved",
|
|
170
|
+
pendingEffect: null
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
return beginEffect(current, "aborting", event.status, "abort_pending");
|
|
174
|
+
}
|
|
175
|
+
if (event.type === "credential_observed") {
|
|
176
|
+
if (event.status === "unknown")
|
|
177
|
+
return decision(current, {}, "none", true);
|
|
178
|
+
if (event.status === "active") {
|
|
179
|
+
return beginEffect(current, "committing", "exchanged", "commit_successor");
|
|
180
|
+
}
|
|
181
|
+
return current.phase === "committed" || current.state === "exchanged" ? beginEffect(current, "aborting", "failed", "revoke_active") : decision(current, {});
|
|
182
|
+
}
|
|
183
|
+
if (event.type === "cancel") {
|
|
184
|
+
return current.phase === "committed" || current.phase === "aborted" ? decision(current, {}) : beginEffect(current, "aborting", "canceled", "abort_pending");
|
|
185
|
+
}
|
|
186
|
+
return current.phase === "aborted" ? decision(current, {}) : beginEffect(current, "aborting", "revoked", "revoke_active");
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
// packages/koed-server/dist/upstream-enrollment.js
|
|
190
|
+
var depsWithDefaults = (deps = {}) => ({
|
|
191
|
+
existsSync: deps.existsSync ?? existsSync,
|
|
192
|
+
readFileSync: deps.readFileSync ?? readFileSync,
|
|
193
|
+
writeFileSync: deps.writeFileSync ?? writeFileSync,
|
|
194
|
+
renameSync: deps.renameSync ?? renameSync,
|
|
195
|
+
fetch: deps.fetch ?? globalThis.fetch.bind(globalThis),
|
|
196
|
+
now: deps.now ?? (() => /* @__PURE__ */ new Date()),
|
|
197
|
+
randomId: deps.randomId ?? randomUUID,
|
|
198
|
+
randomBytes: deps.randomBytes ?? randomBytes,
|
|
199
|
+
beforeEnrollmentEffect: deps.beforeEnrollmentEffect ?? (() => void 0),
|
|
200
|
+
sourceOwnerPrincipalId: deps.sourceOwnerPrincipalId ?? "",
|
|
201
|
+
memoryAnswers: deps.memoryAnswers ?? false
|
|
202
|
+
});
|
|
203
|
+
var defaultStore = (now) => ({
|
|
204
|
+
schemaVersion: 1,
|
|
205
|
+
updatedAt: now,
|
|
206
|
+
enrollments: []
|
|
207
|
+
});
|
|
208
|
+
var validateBackendId = (id) => {
|
|
209
|
+
const trimmed = id.trim();
|
|
210
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]{1,63}$/.test(trimmed)) {
|
|
211
|
+
throw new Error("Upstream backend id must be 2-64 characters of letters, numbers, hyphen, or underscore.");
|
|
212
|
+
}
|
|
213
|
+
return trimmed;
|
|
214
|
+
};
|
|
215
|
+
var readStore = (paths, deps) => {
|
|
216
|
+
const now = deps.now().toISOString();
|
|
217
|
+
if (!deps.existsSync(paths.upstreamEnrollmentsPath)) {
|
|
218
|
+
return defaultStore(now);
|
|
219
|
+
}
|
|
220
|
+
try {
|
|
221
|
+
const parsed = JSON.parse(deps.readFileSync(paths.upstreamEnrollmentsPath, "utf8"));
|
|
222
|
+
return {
|
|
223
|
+
schemaVersion: 1,
|
|
224
|
+
updatedAt: typeof parsed.updatedAt === "string" ? parsed.updatedAt : now,
|
|
225
|
+
enrollments: Array.isArray(parsed.enrollments) ? parsed.enrollments.map((record) => normalizeRecord(record, now)) : []
|
|
226
|
+
};
|
|
227
|
+
} catch {
|
|
228
|
+
throw new Error("Upstream enrollment state is malformed.");
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
var normalizeRecord = (record, now) => {
|
|
232
|
+
const requestId = typeof record.requestId === "string" && record.requestId.trim() ? record.requestId.trim().slice(0, 120) : "unknown";
|
|
233
|
+
const state = normalizeEnrollmentState(record.state);
|
|
234
|
+
const transactionKind = record.transaction?.kind === "replacement" || typeof record.rotationCredentialId === "string" && uuidPattern.test(record.rotationCredentialId) && typeof record.rotationCredentialReference === "string" && record.rotationCredentialReference.trim() ? "replacement" : "initial";
|
|
235
|
+
const candidateTransaction = record.transactionState;
|
|
236
|
+
const phase = candidateTransaction?.phase === "prepared" || candidateTransaction?.phase === "awaiting_remote" || candidateTransaction?.phase === "awaiting_exchange" || candidateTransaction?.phase === "committing" || candidateTransaction?.phase === "committed" || candidateTransaction?.phase === "aborting" || candidateTransaction?.phase === "aborted" || candidateTransaction?.phase === "recovery_required" ? candidateTransaction.phase : state === "exchanged" ? "committed" : state === "denied" || state === "expired" || state === "canceled" || state === "revoked" || state === "failed" ? "aborted" : state === "approved" ? "awaiting_exchange" : "awaiting_remote";
|
|
237
|
+
const pendingEffect = candidateTransaction?.pendingEffect === "stage_pending_custody" || candidateTransaction?.pendingEffect === "compensate_pending_custody" || candidateTransaction?.pendingEffect === "record_challenge" || candidateTransaction?.pendingEffect === "commit_successor" || candidateTransaction?.pendingEffect === "abort_pending" || candidateTransaction?.pendingEffect === "revoke_active" ? candidateTransaction.pendingEffect : null;
|
|
238
|
+
const transactionTargetState = candidateTransaction?.state === "pending" || candidateTransaction?.state === "approved" || candidateTransaction?.state === "exchanged" || candidateTransaction?.state === "denied" || candidateTransaction?.state === "expired" || candidateTransaction?.state === "canceled" || candidateTransaction?.state === "revoked" || candidateTransaction?.state === "failed" ? candidateTransaction.state : state;
|
|
239
|
+
const transactionState = {
|
|
240
|
+
id: typeof candidateTransaction?.id === "string" && candidateTransaction.id.trim() ? candidateTransaction.id.trim().slice(0, 120) : requestId,
|
|
241
|
+
generation: Number.isSafeInteger(candidateTransaction?.generation) && candidateTransaction.generation > 0 ? candidateTransaction.generation : 1,
|
|
242
|
+
kind: transactionKind,
|
|
243
|
+
phase,
|
|
244
|
+
state: transactionTargetState,
|
|
245
|
+
pendingEffect
|
|
246
|
+
};
|
|
247
|
+
return {
|
|
248
|
+
backendId: typeof record.backendId === "string" ? validateBackendId(record.backendId) : "missing",
|
|
249
|
+
requestId,
|
|
250
|
+
state,
|
|
251
|
+
activationUrl: typeof record.activationUrl === "string" ? sanitizeActivationUrl(record.activationUrl) : null,
|
|
252
|
+
requestedOperationFamilies: Array.isArray(record.requestedOperationFamilies) ? record.requestedOperationFamilies.filter(isOperationFamily) : [],
|
|
253
|
+
transaction: record.transaction?.kind === "replacement" && uuidPattern.test(record.transaction.predecessorCredentialId) && record.transaction.predecessorCredentialReference.trim() ? {
|
|
254
|
+
kind: "replacement",
|
|
255
|
+
predecessorCredentialId: record.transaction.predecessorCredentialId.toLowerCase(),
|
|
256
|
+
predecessorCredentialReference: record.transaction.predecessorCredentialReference.trim().slice(0, 180)
|
|
257
|
+
} : typeof record.rotationCredentialId === "string" && uuidPattern.test(record.rotationCredentialId) && typeof record.rotationCredentialReference === "string" && record.rotationCredentialReference.trim() ? {
|
|
258
|
+
kind: "replacement",
|
|
259
|
+
predecessorCredentialId: record.rotationCredentialId.toLowerCase(),
|
|
260
|
+
predecessorCredentialReference: record.rotationCredentialReference.trim().slice(0, 180)
|
|
261
|
+
} : { kind: "initial" },
|
|
262
|
+
transactionState,
|
|
263
|
+
...typeof record.activeCredentialReference === "string" && record.activeCredentialReference.trim() ? {
|
|
264
|
+
activeCredentialReference: record.activeCredentialReference.trim().slice(0, 180)
|
|
265
|
+
} : {},
|
|
266
|
+
...typeof record.pendingCredentialReference === "string" && record.pendingCredentialReference.trim() ? {
|
|
267
|
+
pendingCredentialReference: record.pendingCredentialReference.trim().slice(0, 180)
|
|
268
|
+
} : {},
|
|
269
|
+
...typeof record.challengeHash === "string" && /^[0-9a-f]{64}$/.test(record.challengeHash) ? { challengeHash: record.challengeHash } : {},
|
|
270
|
+
...typeof record.challengeId === "string" && record.challengeId.trim() ? { challengeId: record.challengeId.trim().slice(0, 120) } : {},
|
|
271
|
+
...typeof record.credentialKeyId === "string" && record.credentialKeyId.trim() ? { credentialKeyId: record.credentialKeyId.trim().slice(0, 160) } : {},
|
|
272
|
+
...typeof record.credentialReference === "string" && record.credentialReference.trim() ? { credentialReference: record.credentialReference.trim().slice(0, 180) } : {},
|
|
273
|
+
...typeof record.localClientCredentialReference === "string" && record.localClientCredentialReference.trim() ? {
|
|
274
|
+
localClientCredentialReference: record.localClientCredentialReference.trim().slice(0, 200)
|
|
275
|
+
} : {},
|
|
276
|
+
...typeof record.deviceCredentialId === "string" && uuidPattern.test(record.deviceCredentialId) ? { deviceCredentialId: record.deviceCredentialId.toLowerCase() } : {},
|
|
277
|
+
...typeof record.principalUserId === "string" && uuidPattern.test(record.principalUserId) ? { principalUserId: record.principalUserId.toLowerCase() } : {},
|
|
278
|
+
createdAt: typeof record.createdAt === "string" ? record.createdAt : now,
|
|
279
|
+
updatedAt: typeof record.updatedAt === "string" ? record.updatedAt : now,
|
|
280
|
+
expiresAt: typeof record.expiresAt === "string" ? record.expiresAt : null,
|
|
281
|
+
...typeof record.failureReason === "string" ? { failureReason: record.failureReason.slice(0, 80) } : {},
|
|
282
|
+
...typeof record.failureMessage === "string" ? { failureMessage: record.failureMessage.slice(0, 240) } : {},
|
|
283
|
+
credential: sanitizeCredential(record.credential)
|
|
284
|
+
};
|
|
285
|
+
};
|
|
286
|
+
var replacementTransaction = (record) => record.transaction.kind === "replacement" ? record.transaction : null;
|
|
287
|
+
var normalizeEnrollmentState = (value) => {
|
|
288
|
+
if (value === "approved" || value === "exchanged" || value === "denied" || value === "expired" || value === "canceled" || value === "revoked" || value === "failed") {
|
|
289
|
+
return value;
|
|
290
|
+
}
|
|
291
|
+
return "pending";
|
|
292
|
+
};
|
|
293
|
+
var uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
294
|
+
var sanitizeActivationUrl = (value) => {
|
|
295
|
+
try {
|
|
296
|
+
const parsed = new URL(value);
|
|
297
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
assertSecureHttpTransport(parsed, "Activation URL");
|
|
301
|
+
parsed.username = "";
|
|
302
|
+
parsed.password = "";
|
|
303
|
+
for (const key of [...parsed.searchParams.keys()]) {
|
|
304
|
+
if (isSecretKey(key)) {
|
|
305
|
+
parsed.searchParams.set(key, "[redacted]");
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return parsed.toString();
|
|
309
|
+
} catch {
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
var sanitizeCredential = (credential) => {
|
|
314
|
+
if (credential?.status === "configured" || credential?.status === "revoked" || credential?.status === "unknown") {
|
|
315
|
+
return {
|
|
316
|
+
status: credential.status,
|
|
317
|
+
...typeof credential.reference === "string" && credential.reference.trim() && !isSecretKey(credential.reference) ? { reference: credential.reference.trim().slice(0, 240) } : {}
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
return { status: "not_configured" };
|
|
321
|
+
};
|
|
322
|
+
var isSecretKey = (value) => /(?:token|secret|password|bearer|cookie|authorization|verifier|challenge)/i.test(value);
|
|
323
|
+
var isOperationFamily = (value) => typeof value === "string" && /^[a-z0-9_.:-]{1,80}$/i.test(value) && !isSecretKey(value);
|
|
324
|
+
var writeStore = (paths, store, deps) => {
|
|
325
|
+
mkdirSync2(dirname(paths.upstreamEnrollmentsPath), {
|
|
326
|
+
recursive: true,
|
|
327
|
+
mode: 448
|
|
328
|
+
});
|
|
329
|
+
const tempPath = `${paths.upstreamEnrollmentsPath}.tmp`;
|
|
330
|
+
deps.writeFileSync(tempPath, `${JSON.stringify(store, null, 2)}
|
|
331
|
+
`, {
|
|
332
|
+
mode: 384
|
|
333
|
+
});
|
|
334
|
+
deps.renameSync(tempPath, paths.upstreamEnrollmentsPath);
|
|
335
|
+
};
|
|
336
|
+
var summarizeEnrollment = (record, backend) => {
|
|
337
|
+
const credential = record.state === "revoked" ? record.credential ?? { status: "revoked" } : backend?.credential.status === "configured" || backend?.credential.status === "revoked" ? backend.credential : record.credential ?? backend?.credential ?? { status: "unknown" };
|
|
338
|
+
return {
|
|
339
|
+
backendId: record.backendId,
|
|
340
|
+
requestId: record.requestId,
|
|
341
|
+
state: record.state,
|
|
342
|
+
activationUrl: record.activationUrl,
|
|
343
|
+
requestedOperationFamilies: record.requestedOperationFamilies,
|
|
344
|
+
...record.challengeId ? { challengeId: record.challengeId } : {},
|
|
345
|
+
...record.deviceCredentialId ? { deviceCredentialId: record.deviceCredentialId } : {},
|
|
346
|
+
...record.principalUserId ? { principalUserId: record.principalUserId } : {},
|
|
347
|
+
createdAt: record.createdAt,
|
|
348
|
+
updatedAt: record.updatedAt,
|
|
349
|
+
expiresAt: record.expiresAt,
|
|
350
|
+
...record.failureReason ? { failureReason: record.failureReason } : {},
|
|
351
|
+
...record.failureMessage ? { failureMessage: record.failureMessage } : {},
|
|
352
|
+
credential
|
|
353
|
+
};
|
|
354
|
+
};
|
|
355
|
+
var routePolicyOperationFamilies = (routePolicy, options = { collaboration: false }) => {
|
|
356
|
+
const entries = [
|
|
357
|
+
["personalMemoryRead", "personal_memory_read"],
|
|
358
|
+
...options.collaboration ? [
|
|
359
|
+
["personalCollaboration", "personal_collaboration_read"],
|
|
360
|
+
["personalCollaboration", "personal_collaboration_write"]
|
|
361
|
+
] : [],
|
|
362
|
+
["teamWorkspaceRead", "team_workspace_read"],
|
|
363
|
+
["memoryAnswerExecution", "memory_answer_execution"],
|
|
364
|
+
...options.collaboration ? [
|
|
365
|
+
["teamWorkspaceRead", "team_chat_read"],
|
|
366
|
+
["teamWorkspaceRead", "team_chat_write"]
|
|
367
|
+
] : [],
|
|
368
|
+
["shareGrantManagement", "share_grant_management"],
|
|
369
|
+
["captureWrites", "capture_writes"],
|
|
370
|
+
["sync", "sync"],
|
|
371
|
+
["managedExecution", "managed_execution"],
|
|
372
|
+
["managedExecution", "managed_file_read"],
|
|
373
|
+
["managedExecution", "managed_terminal"],
|
|
374
|
+
["admin", "action_grant"]
|
|
375
|
+
];
|
|
376
|
+
return entries.filter(([key]) => routePolicy[key] === "enabled").map(([, family]) => family);
|
|
377
|
+
};
|
|
378
|
+
var browserEnrollmentOperationFamilies = (routePolicy, options) => routePolicyOperationFamilies({
|
|
379
|
+
...routePolicy,
|
|
380
|
+
...options.memoryAnswers ? { memoryAnswerExecution: "enabled" } : {}
|
|
381
|
+
}, options);
|
|
382
|
+
var localClientOperationFamiliesFor = (operationFamilies) => operationFamilies.filter((family) => family === "personal_collaboration_read" || family === "personal_collaboration_write" || family === "team_workspace_read" || family === "memory_answer_execution" || family === "team_chat_read" || family === "team_chat_write" || family === "share_grant_management" || family === "managed_execution" || family === "managed_file_read" || family === "managed_terminal");
|
|
383
|
+
var expiresAtFor = (now) => new Date(now.getTime() + 10 * 60 * 1e3).toISOString();
|
|
384
|
+
var randomSecret = (deps, bytes = 32) => deps.randomBytes(bytes).toString("base64url");
|
|
385
|
+
var challengeHashFor = (backendId, requestId, secret) => createHash2("sha256").update(`${backendId}:${requestId}:${secret}`).digest("hex");
|
|
386
|
+
var approvalUrlFor = (backend, challengeId) => new URL(`device-enrollment/${encodeURIComponent(challengeId)}`, `${backend.baseUrl}/`).toString();
|
|
387
|
+
var jsonFetch = async (deps, url, init) => {
|
|
388
|
+
const response = await deps.fetch(url, {
|
|
389
|
+
...init,
|
|
390
|
+
redirect: "error",
|
|
391
|
+
headers: {
|
|
392
|
+
accept: "application/json",
|
|
393
|
+
...init.body === void 0 ? {} : { "content-type": "application/json" },
|
|
394
|
+
...init.headers
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
const payload = await response.json().catch(() => ({}));
|
|
398
|
+
return { status: response.status, ok: response.ok, payload };
|
|
399
|
+
};
|
|
400
|
+
var createUpstreamChallenge = async (backend, input, deps) => {
|
|
401
|
+
const result = await jsonFetch(deps, new URL("v1/local-edge/device-enrollments/challenges", `${backend.baseUrl}/`), {
|
|
402
|
+
method: "POST",
|
|
403
|
+
headers: input.rotation ? { authorization: input.rotation.authorization } : void 0,
|
|
404
|
+
body: JSON.stringify({
|
|
405
|
+
challenge_hash: input.challengeHash,
|
|
406
|
+
upstream_backend_id: input.backendId,
|
|
407
|
+
device_instance_id: input.deviceInstanceId,
|
|
408
|
+
protocol_deployment_id: input.protocolDeploymentId,
|
|
409
|
+
...input.sourceOwnerPrincipalId ? { source_owner_principal_id: input.sourceOwnerPrincipalId } : {},
|
|
410
|
+
...input.rotation ? { rotate_credential_id: input.rotation.credentialId } : {},
|
|
411
|
+
device_label: "Koed local edge",
|
|
412
|
+
requested_operation_families: input.operationFamilies,
|
|
413
|
+
pending_credential: {
|
|
414
|
+
credential_key_id: input.credentialKeyId,
|
|
415
|
+
verifier_kind: "secret_hash",
|
|
416
|
+
verifier_secret: input.verifierSecret,
|
|
417
|
+
operation_families: input.operationFamilies
|
|
418
|
+
},
|
|
419
|
+
metadata: {
|
|
420
|
+
source: "koed-server-upstream-enroll",
|
|
421
|
+
requestId: input.requestId
|
|
422
|
+
},
|
|
423
|
+
ttl_seconds: 600
|
|
424
|
+
})
|
|
425
|
+
});
|
|
426
|
+
if (!result.ok) {
|
|
427
|
+
throw new Error(typeof result.payload.error === "string" ? result.payload.error : `Upstream challenge creation failed with HTTP ${result.status}.`);
|
|
428
|
+
}
|
|
429
|
+
const challenge = result.payload.challenge;
|
|
430
|
+
const rawChallengeId = challenge && typeof challenge === "object" && "id" in challenge ? challenge.id : "";
|
|
431
|
+
const challengeId = typeof rawChallengeId === "string" ? rawChallengeId : "";
|
|
432
|
+
if (!challengeId) {
|
|
433
|
+
throw new Error("Upstream challenge response did not include a challenge id.");
|
|
434
|
+
}
|
|
435
|
+
const rawActivationUrl = result.payload.activationUrl;
|
|
436
|
+
const activationUrl = typeof rawActivationUrl === "string" ? sanitizeActivationUrl(rawActivationUrl) : null;
|
|
437
|
+
return {
|
|
438
|
+
challengeId,
|
|
439
|
+
activationUrl: activationUrl ?? approvalUrlFor(backend, challengeId)
|
|
440
|
+
};
|
|
441
|
+
};
|
|
442
|
+
var readUpstreamChallengeStatus = async (backend, challengeId, deviceInstanceId, deps) => {
|
|
443
|
+
const result = await jsonFetch(deps, new URL(`v1/local-edge/device-enrollments/challenges/${encodeURIComponent(challengeId)}`, `${backend.baseUrl}/`), {
|
|
444
|
+
method: "GET",
|
|
445
|
+
headers: { "x-koed-device-instance-id": deviceInstanceId }
|
|
446
|
+
});
|
|
447
|
+
if (!result.ok) {
|
|
448
|
+
return "unknown";
|
|
449
|
+
}
|
|
450
|
+
const challenge = result.payload.challenge;
|
|
451
|
+
const rawStatus = challenge && typeof challenge === "object" && "status" in challenge ? challenge.status : "";
|
|
452
|
+
const status = typeof rawStatus === "string" ? rawStatus : "";
|
|
453
|
+
return status === "pending" || status === "approved" || status === "denied" || status === "expired" ? status : "unknown";
|
|
454
|
+
};
|
|
455
|
+
var remoteCredentialStatus = async (paths, backend, reference, deps) => {
|
|
456
|
+
const authorization = readUpstreamCredentialAuthorization(paths.koedHome, reference);
|
|
457
|
+
if (!authorization) {
|
|
458
|
+
return { state: "unknown" };
|
|
459
|
+
}
|
|
460
|
+
try {
|
|
461
|
+
const result = await jsonFetch(deps, new URL("v1/local-edge/device-credentials/status", `${backend.baseUrl}/`), {
|
|
462
|
+
method: "GET",
|
|
463
|
+
headers: { authorization }
|
|
464
|
+
});
|
|
465
|
+
if (result.status === 401 || result.status === 403) {
|
|
466
|
+
return { state: "rejected" };
|
|
467
|
+
}
|
|
468
|
+
const credential = result.payload.credential;
|
|
469
|
+
const user = result.payload.user;
|
|
470
|
+
const deviceCredentialId = credential && typeof credential === "object" && "id" in credential ? credential.id : null;
|
|
471
|
+
const principalUserId = user && typeof user === "object" && "id" in user ? user.id : null;
|
|
472
|
+
return result.ok && result.payload.ok === true && typeof deviceCredentialId === "string" && uuidPattern.test(deviceCredentialId) && typeof principalUserId === "string" && uuidPattern.test(principalUserId) ? {
|
|
473
|
+
state: "active",
|
|
474
|
+
deviceCredentialId: deviceCredentialId.toLowerCase(),
|
|
475
|
+
principalUserId: principalUserId.toLowerCase()
|
|
476
|
+
} : { state: "unknown" };
|
|
477
|
+
} catch {
|
|
478
|
+
return { state: "unknown" };
|
|
479
|
+
}
|
|
480
|
+
};
|
|
481
|
+
var discoverRotationCredential = async (paths, backend, store, deps) => {
|
|
482
|
+
const candidates = [...store.enrollments].reverse().filter((record) => record.backendId === backend.id && typeof record.credentialReference === "string");
|
|
483
|
+
for (const candidate of candidates) {
|
|
484
|
+
const credentialReference = candidate.credentialReference;
|
|
485
|
+
const authorization = readUpstreamCredentialAuthorization(paths.koedHome, credentialReference);
|
|
486
|
+
if (!authorization)
|
|
487
|
+
continue;
|
|
488
|
+
const status = await remoteCredentialStatus(paths, backend, credentialReference, deps);
|
|
489
|
+
if (status.state === "active") {
|
|
490
|
+
return {
|
|
491
|
+
state: "active",
|
|
492
|
+
credentialId: status.deviceCredentialId,
|
|
493
|
+
principalUserId: status.principalUserId,
|
|
494
|
+
credentialReference,
|
|
495
|
+
authorization
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
if (status.state === "unknown")
|
|
499
|
+
return { state: "unavailable" };
|
|
500
|
+
}
|
|
501
|
+
return { state: "none" };
|
|
502
|
+
};
|
|
503
|
+
var revokeRemoteCredential = async (paths, backend, reference, deps) => {
|
|
504
|
+
const authorization = readUpstreamCredentialAuthorization(paths.koedHome, reference);
|
|
505
|
+
if (!authorization)
|
|
506
|
+
return "unavailable";
|
|
507
|
+
try {
|
|
508
|
+
const result = await jsonFetch(deps, new URL("v1/local-edge/device-credentials/current", `${backend.baseUrl}/`), {
|
|
509
|
+
method: "DELETE",
|
|
510
|
+
headers: { authorization }
|
|
511
|
+
});
|
|
512
|
+
if (result.ok && result.payload.revoked === true)
|
|
513
|
+
return "revoked";
|
|
514
|
+
if (result.status === 401)
|
|
515
|
+
return "already_inactive";
|
|
516
|
+
return "unavailable";
|
|
517
|
+
} catch {
|
|
518
|
+
return "unavailable";
|
|
519
|
+
}
|
|
520
|
+
};
|
|
521
|
+
var latestEnrollment = (store, backendId) => [...store.enrollments].reverse().find((enrollment) => enrollment.backendId === backendId);
|
|
522
|
+
var readUpstreamEnrollmentBinding = (paths, id, deps = {}) => readLocalEdgeUpstreamEnrollmentBinding(paths.upstreamEnrollmentsPath, validateBackendId(id), {
|
|
523
|
+
existsSync: deps.existsSync,
|
|
524
|
+
readFileSync: deps.readFileSync
|
|
525
|
+
});
|
|
526
|
+
var backendById = (paths, backendId, deps) => {
|
|
527
|
+
const registry = collectUpstreamRegistryStatus(paths, {
|
|
528
|
+
existsSync: deps.existsSync,
|
|
529
|
+
readFileSync: deps.readFileSync,
|
|
530
|
+
now: deps.now
|
|
531
|
+
});
|
|
532
|
+
return {
|
|
533
|
+
backend: registry.backends.find((backend) => backend.id === backendId),
|
|
534
|
+
parseError: registry.parseError
|
|
535
|
+
};
|
|
536
|
+
};
|
|
537
|
+
var enrollmentCanReceiveRemoteStatus = (record) => record.state === "pending" || record.state === "approved" || record.state === "exchanged";
|
|
538
|
+
var sameEnrollmentIdentity = (current, expected) => current?.requestId === expected.requestId && current.credentialReference === expected.credentialReference && upstreamEnrollmentObservationApplies(current.transactionState, {
|
|
539
|
+
transactionId: expected.transactionState.id,
|
|
540
|
+
generation: expected.transactionState.generation
|
|
541
|
+
});
|
|
542
|
+
var enrollmentResultFromSnapshot = (backendId, record, backend) => record ? {
|
|
543
|
+
ok: true,
|
|
544
|
+
state: record.state,
|
|
545
|
+
backend,
|
|
546
|
+
enrollment: summarizeEnrollment(record, backend),
|
|
547
|
+
message: `Upstream enrollment for ${backendId} is ${record.state}.`
|
|
548
|
+
} : {
|
|
549
|
+
ok: true,
|
|
550
|
+
state: "missing",
|
|
551
|
+
backend,
|
|
552
|
+
message: `No upstream enrollment has been started for ${backendId}.`
|
|
553
|
+
};
|
|
554
|
+
var materializeState = (record, now, backend) => {
|
|
555
|
+
const replacement = replacementTransaction(record);
|
|
556
|
+
if (backend?.credential.status === "configured" && (!replacement || backend.credential.reference === record.credentialReference)) {
|
|
557
|
+
return {
|
|
558
|
+
...record,
|
|
559
|
+
state: "exchanged",
|
|
560
|
+
credential: backend.credential
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
if (backend?.credential.status === "revoked") {
|
|
564
|
+
return {
|
|
565
|
+
...record,
|
|
566
|
+
state: "revoked",
|
|
567
|
+
credential: backend.credential
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
if (record.state === "exchanged" && backend?.credential.status === "not_configured") {
|
|
571
|
+
return {
|
|
572
|
+
...record,
|
|
573
|
+
state: "failed",
|
|
574
|
+
updatedAt: now.toISOString(),
|
|
575
|
+
failureReason: "credential_reset",
|
|
576
|
+
failureMessage: "Upstream backend credential is no longer configured; restart enrollment.",
|
|
577
|
+
credential: backend.credential
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
if (record.state === "denied" || record.state === "canceled" || record.state === "expired" || record.state === "failed") {
|
|
581
|
+
return record;
|
|
582
|
+
}
|
|
583
|
+
if ((record.state === "pending" || record.state === "approved") && record.expiresAt && Date.parse(record.expiresAt) <= now.getTime()) {
|
|
584
|
+
return {
|
|
585
|
+
...record,
|
|
586
|
+
state: "expired",
|
|
587
|
+
updatedAt: now.toISOString()
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
return record;
|
|
591
|
+
};
|
|
592
|
+
var withSecureFetchForBackend = async (paths, id, deps, operation) => {
|
|
593
|
+
if (deps.fetch)
|
|
594
|
+
return operation(deps);
|
|
595
|
+
const backend = backendById(paths, validateBackendId(id), depsWithDefaults(deps)).backend;
|
|
596
|
+
if (!backend)
|
|
597
|
+
return operation(deps);
|
|
598
|
+
const secureFetch = createSecureUpstreamFetch({
|
|
599
|
+
allowPrivateNetworkForUrl: registeredPrivateNetworkPolicy(() => [
|
|
600
|
+
{ baseUrl: backend.baseUrl, profile: backend.profile }
|
|
601
|
+
])
|
|
602
|
+
});
|
|
603
|
+
try {
|
|
604
|
+
return await operation({ ...deps, fetch: secureFetch });
|
|
605
|
+
} finally {
|
|
606
|
+
await secureFetch.close();
|
|
607
|
+
}
|
|
608
|
+
};
|
|
609
|
+
var startUpstreamEnrollmentWithFetch = async (paths, id, deps = {}) => {
|
|
610
|
+
const resolvedDeps = depsWithDefaults(deps);
|
|
611
|
+
const backendId = validateBackendId(id);
|
|
612
|
+
const identity = await ensureDeviceIdentity(paths);
|
|
613
|
+
if (!identity.remoteOperationsAllowed) {
|
|
614
|
+
return {
|
|
615
|
+
ok: false,
|
|
616
|
+
state: "failed",
|
|
617
|
+
message: `Local device identity is ${identity.health}; upstream enrollment is blocked until explicit identity rotation.`
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
const now = resolvedDeps.now();
|
|
621
|
+
const nowIso = now.toISOString();
|
|
622
|
+
const { backend, parseError } = backendById(paths, backendId, resolvedDeps);
|
|
623
|
+
if (parseError) {
|
|
624
|
+
return {
|
|
625
|
+
ok: false,
|
|
626
|
+
state: "failed",
|
|
627
|
+
message: "Upstream backend registry is malformed."
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
if (!backend) {
|
|
631
|
+
return {
|
|
632
|
+
ok: false,
|
|
633
|
+
state: "missing",
|
|
634
|
+
message: `Upstream backend ${backendId} is not registered.`
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
if (upstreamDisconnectCleanupPending(paths, backendId)) {
|
|
638
|
+
return {
|
|
639
|
+
ok: false,
|
|
640
|
+
state: "failed",
|
|
641
|
+
backend,
|
|
642
|
+
message: `Upstream backend ${backendId} cannot enroll until disconnect cleanup completes.`
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
if (backend.capabilities.state !== "validated") {
|
|
646
|
+
return {
|
|
647
|
+
ok: false,
|
|
648
|
+
state: "failed",
|
|
649
|
+
backend,
|
|
650
|
+
message: `Upstream backend ${backendId} capabilities are not validated. Run koed upstream refresh --id ${backendId} --json.`
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
if (resolvedDeps.memoryAnswers || backend.routePolicy.memoryAnswerExecution === "enabled") {
|
|
654
|
+
if (backend.routePolicy.teamWorkspaceRead !== "enabled") {
|
|
655
|
+
return {
|
|
656
|
+
ok: false,
|
|
657
|
+
state: "failed",
|
|
658
|
+
backend,
|
|
659
|
+
message: "Memory Answer enrollment requires explicit Team Workspace read routing first."
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
if (!upstreamBackendSupportsTeamMemoryAnswerTasks(paths, backendId, resolvedDeps)) {
|
|
663
|
+
return {
|
|
664
|
+
ok: false,
|
|
665
|
+
state: "failed",
|
|
666
|
+
backend,
|
|
667
|
+
message: `The backend must advertise memory.answerTasks version 2 with Team support. Upgrade it, run koed upstream refresh --id ${backendId} --json, then re-enroll with --memory-answers. Team read alone does not authorize execution.`
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
const configuredOperationFamilies = routePolicyOperationFamilies(backend.routePolicy);
|
|
672
|
+
const collaboration = upstreamBackendAdvertisesCapability(paths, backendId, "memory.collaboration", {
|
|
673
|
+
existsSync: resolvedDeps.existsSync,
|
|
674
|
+
readFileSync: resolvedDeps.readFileSync,
|
|
675
|
+
now: resolvedDeps.now
|
|
676
|
+
});
|
|
677
|
+
const operationFamilies = browserEnrollmentOperationFamilies(backend.routePolicy, { collaboration, memoryAnswers: resolvedDeps.memoryAnswers });
|
|
678
|
+
if (configuredOperationFamilies.length === 0) {
|
|
679
|
+
return {
|
|
680
|
+
ok: false,
|
|
681
|
+
state: "failed",
|
|
682
|
+
backend,
|
|
683
|
+
message: `Upstream backend ${backendId} has no enabled route-policy families.`
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
if (operationFamilies.length === 0) {
|
|
687
|
+
return {
|
|
688
|
+
ok: false,
|
|
689
|
+
state: "failed",
|
|
690
|
+
backend,
|
|
691
|
+
message: `Upstream backend ${backendId} has no browser-enrollable route-policy families.`
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
if (operationFamilies.includes("share_grant_management") && !uuidPattern.test(resolvedDeps.sourceOwnerPrincipalId)) {
|
|
695
|
+
return {
|
|
696
|
+
ok: false,
|
|
697
|
+
state: "failed",
|
|
698
|
+
backend,
|
|
699
|
+
message: "Share Grant enrollment requires the authenticated local owner principal."
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
let store = readStore(paths, resolvedDeps);
|
|
703
|
+
let expectedRecord = latestEnrollment(store, backendId);
|
|
704
|
+
if (expectedRecord) {
|
|
705
|
+
const current = await getUpstreamEnrollmentStatus(paths, backendId, resolvedDeps);
|
|
706
|
+
if (current.state === "pending" || current.state === "approved" || current.state === "exchanged" && JSON.stringify(current.enrollment?.requestedOperationFamilies ?? []) === JSON.stringify(operationFamilies)) {
|
|
707
|
+
if (resolvedDeps.memoryAnswers && !current.enrollment?.requestedOperationFamilies.includes("memory_answer_execution")) {
|
|
708
|
+
return {
|
|
709
|
+
...current,
|
|
710
|
+
ok: false,
|
|
711
|
+
message: `Cancel the existing enrollment, then run koed upstream enroll start --id ${backendId} --memory-answers to request Memory Answer execution explicitly.`
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
return current;
|
|
715
|
+
}
|
|
716
|
+
store = readStore(paths, resolvedDeps);
|
|
717
|
+
expectedRecord = latestEnrollment(store, backendId);
|
|
718
|
+
}
|
|
719
|
+
const rotation = await discoverRotationCredential(paths, backend, store, resolvedDeps);
|
|
720
|
+
if (rotation.state === "unavailable") {
|
|
721
|
+
return {
|
|
722
|
+
ok: false,
|
|
723
|
+
state: "failed",
|
|
724
|
+
backend,
|
|
725
|
+
message: "Could not verify the existing upstream device credential required for authenticated rotation. Retry when the Team Backend is available."
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
const requestId = resolvedDeps.randomId();
|
|
729
|
+
const deviceInstanceId = identity.deviceInstanceId;
|
|
730
|
+
const verifierSecret = randomSecret(resolvedDeps);
|
|
731
|
+
const challengeSecret = randomSecret(resolvedDeps);
|
|
732
|
+
const challengeHash = challengeHashFor(backendId, requestId, challengeSecret);
|
|
733
|
+
const credentialKeyId = `koed_${createHash2("sha256").update(`${backendId}:${requestId}:${verifierSecret}`).digest("hex").slice(0, 40)}`;
|
|
734
|
+
const expiresAt = expiresAtFor(now);
|
|
735
|
+
const localClientOperationFamilies = localClientOperationFamiliesFor(operationFamilies);
|
|
736
|
+
const localClientSecret = randomSecret(resolvedDeps);
|
|
737
|
+
const staged = await withUpstreamEnrollmentLock(paths, backendId, () => {
|
|
738
|
+
store = readStore(paths, resolvedDeps);
|
|
739
|
+
const currentRecord = latestEnrollment(store, backendId);
|
|
740
|
+
const currentBackend = backendById(paths, backendId, resolvedDeps).backend;
|
|
741
|
+
const expectedStillCurrent = expectedRecord ? sameEnrollmentIdentity(currentRecord, expectedRecord) && currentRecord.state === expectedRecord.state : currentRecord === void 0;
|
|
742
|
+
const currentCollaboration = upstreamBackendAdvertisesCapability(paths, backendId, "memory.collaboration", {
|
|
743
|
+
existsSync: resolvedDeps.existsSync,
|
|
744
|
+
readFileSync: resolvedDeps.readFileSync,
|
|
745
|
+
now: resolvedDeps.now
|
|
746
|
+
});
|
|
747
|
+
const currentOperationFamilies = currentBackend ? browserEnrollmentOperationFamilies(currentBackend.routePolicy, {
|
|
748
|
+
collaboration: currentCollaboration,
|
|
749
|
+
memoryAnswers: resolvedDeps.memoryAnswers
|
|
750
|
+
}) : [];
|
|
751
|
+
if (!expectedStillCurrent || !currentBackend || currentBackend.capabilities.state !== "validated" || JSON.stringify(currentOperationFamilies) !== JSON.stringify(operationFamilies)) {
|
|
752
|
+
return {
|
|
753
|
+
kind: "result",
|
|
754
|
+
result: currentRecord ? enrollmentResultFromSnapshot(backendId, currentRecord, currentBackend) : {
|
|
755
|
+
ok: false,
|
|
756
|
+
state: "failed",
|
|
757
|
+
backend: currentBackend,
|
|
758
|
+
message: `Upstream backend ${backendId} changed while enrollment was starting; retry with its current configuration.`
|
|
759
|
+
}
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
const reference = upstreamCredentialReferenceFor({
|
|
763
|
+
backendId,
|
|
764
|
+
credentialKeyId
|
|
765
|
+
});
|
|
766
|
+
const transactionGeneration = Math.max(0, ...store.enrollments.filter((entry) => entry.backendId === backendId).map((entry) => entry.transactionState.generation)) + 1;
|
|
767
|
+
const preparedTransaction = createUpstreamEnrollmentTransaction({
|
|
768
|
+
id: requestId,
|
|
769
|
+
generation: transactionGeneration,
|
|
770
|
+
kind: rotation.state === "active" ? "replacement" : "initial"
|
|
771
|
+
});
|
|
772
|
+
const stageDecision = decideUpstreamEnrollmentTransaction(preparedTransaction, { type: "prepare" });
|
|
773
|
+
const record = {
|
|
774
|
+
backendId,
|
|
775
|
+
requestId,
|
|
776
|
+
state: "pending",
|
|
777
|
+
activationUrl: null,
|
|
778
|
+
requestedOperationFamilies: operationFamilies,
|
|
779
|
+
transaction: rotation.state === "active" ? {
|
|
780
|
+
kind: "replacement",
|
|
781
|
+
predecessorCredentialId: rotation.credentialId,
|
|
782
|
+
predecessorCredentialReference: rotation.credentialReference
|
|
783
|
+
} : { kind: "initial" },
|
|
784
|
+
transactionState: stageDecision.next,
|
|
785
|
+
...rotation.state === "active" ? { activeCredentialReference: rotation.credentialReference } : {},
|
|
786
|
+
pendingCredentialReference: reference,
|
|
787
|
+
challengeHash,
|
|
788
|
+
credentialKeyId,
|
|
789
|
+
credentialReference: reference,
|
|
790
|
+
createdAt: nowIso,
|
|
791
|
+
updatedAt: nowIso,
|
|
792
|
+
expiresAt,
|
|
793
|
+
credential: { status: "unknown", reference }
|
|
794
|
+
};
|
|
795
|
+
if (rotation.state === "active") {
|
|
796
|
+
store.enrollments = store.enrollments.map((entry) => entry.backendId === backendId && entry.credentialReference === rotation.credentialReference ? {
|
|
797
|
+
...entry,
|
|
798
|
+
state: "exchanged",
|
|
799
|
+
updatedAt: nowIso,
|
|
800
|
+
deviceCredentialId: rotation.credentialId,
|
|
801
|
+
principalUserId: rotation.principalUserId,
|
|
802
|
+
credential: {
|
|
803
|
+
status: "configured",
|
|
804
|
+
reference: rotation.credentialReference
|
|
805
|
+
}
|
|
806
|
+
} : entry);
|
|
807
|
+
}
|
|
808
|
+
store.enrollments.push(record);
|
|
809
|
+
store.updatedAt = nowIso;
|
|
810
|
+
writeStore(paths, store, resolvedDeps);
|
|
811
|
+
try {
|
|
812
|
+
let stagedRecord = record;
|
|
813
|
+
executeUpstreamEnrollmentTransactionEffect(stageDecision, {
|
|
814
|
+
stage_pending_custody: () => {
|
|
815
|
+
resolvedDeps.beforeEnrollmentEffect("stage_upstream_credential");
|
|
816
|
+
if (rotation.state !== "active" && localClientOperationFamilies.length > 0) {
|
|
817
|
+
resolvedDeps.beforeEnrollmentEffect("stage_local_client_credential");
|
|
818
|
+
const custody = storeEnrollmentCredentialCustody(paths.koedHome, {
|
|
819
|
+
upstream: { backendId, credentialKeyId, secret: verifierSecret },
|
|
820
|
+
localEdgeClient: {
|
|
821
|
+
backendId,
|
|
822
|
+
secret: localClientSecret,
|
|
823
|
+
operationFamilies: localClientOperationFamilies
|
|
824
|
+
}
|
|
825
|
+
});
|
|
826
|
+
if (custody.upstreamReference !== reference) {
|
|
827
|
+
throw new Error("Staged upstream credential reference changed.");
|
|
828
|
+
}
|
|
829
|
+
stagedRecord = {
|
|
830
|
+
...stagedRecord,
|
|
831
|
+
localClientCredentialReference: custody.localEdgeClientReference
|
|
832
|
+
};
|
|
833
|
+
} else {
|
|
834
|
+
const stored = storeUpstreamCredentialSecret(paths.koedHome, {
|
|
835
|
+
backendId,
|
|
836
|
+
credentialKeyId,
|
|
837
|
+
secret: verifierSecret
|
|
838
|
+
});
|
|
839
|
+
if (stored.reference !== reference) {
|
|
840
|
+
throw new Error("Staged upstream credential reference changed.");
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
resolvedDeps.beforeEnrollmentEffect("stage_registry_credential");
|
|
844
|
+
updateUpstreamBackendCredential(paths, backendId, rotation.state === "active" ? {
|
|
845
|
+
status: "configured",
|
|
846
|
+
reference: rotation.credentialReference
|
|
847
|
+
} : { status: "unknown", reference }, {
|
|
848
|
+
existsSync: resolvedDeps.existsSync,
|
|
849
|
+
readFileSync: resolvedDeps.readFileSync,
|
|
850
|
+
writeFileSync: resolvedDeps.writeFileSync,
|
|
851
|
+
renameSync: resolvedDeps.renameSync,
|
|
852
|
+
now: resolvedDeps.now
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
});
|
|
856
|
+
stagedRecord = {
|
|
857
|
+
...stagedRecord,
|
|
858
|
+
transactionState: decideUpstreamEnrollmentTransaction(stageDecision.next, { type: "effect_succeeded" }).next,
|
|
859
|
+
updatedAt: resolvedDeps.now().toISOString()
|
|
860
|
+
};
|
|
861
|
+
store.enrollments = store.enrollments.map((entry) => entry.backendId === record.backendId && entry.requestId === record.requestId ? stagedRecord : entry);
|
|
862
|
+
store.updatedAt = stagedRecord.updatedAt;
|
|
863
|
+
resolvedDeps.beforeEnrollmentEffect("commit_prepared_state");
|
|
864
|
+
writeStore(paths, store, resolvedDeps);
|
|
865
|
+
return { kind: "record", record: stagedRecord };
|
|
866
|
+
} catch (error) {
|
|
867
|
+
const recoveryRequired = {
|
|
868
|
+
...record,
|
|
869
|
+
transactionState: decideUpstreamEnrollmentTransaction(stageDecision.next, { type: "effect_failed" }).next,
|
|
870
|
+
updatedAt: resolvedDeps.now().toISOString(),
|
|
871
|
+
failureReason: "prepared_effect_interrupted",
|
|
872
|
+
failureMessage: error instanceof Error ? error.message.slice(0, 240) : "Enrollment preparation was interrupted."
|
|
873
|
+
};
|
|
874
|
+
store.enrollments = store.enrollments.map((entry) => entry.backendId === record.backendId && entry.requestId === record.requestId ? recoveryRequired : entry);
|
|
875
|
+
store.updatedAt = recoveryRequired.updatedAt;
|
|
876
|
+
writeStore(paths, store, resolvedDeps);
|
|
877
|
+
return {
|
|
878
|
+
kind: "result",
|
|
879
|
+
result: {
|
|
880
|
+
ok: false,
|
|
881
|
+
state: "failed",
|
|
882
|
+
backend: backendById(paths, backendId, resolvedDeps).backend,
|
|
883
|
+
enrollment: summarizeEnrollment(recoveryRequired, backendById(paths, backendId, resolvedDeps).backend),
|
|
884
|
+
message: "Upstream enrollment preparation was interrupted and requires recovery."
|
|
885
|
+
}
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
});
|
|
889
|
+
if (staged.kind === "result")
|
|
890
|
+
return staged.result;
|
|
891
|
+
expectedRecord = staged.record;
|
|
892
|
+
let challenge;
|
|
893
|
+
try {
|
|
894
|
+
challenge = await createUpstreamChallenge(backend, {
|
|
895
|
+
backendId,
|
|
896
|
+
deviceInstanceId,
|
|
897
|
+
protocolDeploymentId: identity.deploymentId,
|
|
898
|
+
sourceOwnerPrincipalId: resolvedDeps.sourceOwnerPrincipalId,
|
|
899
|
+
challengeHash,
|
|
900
|
+
credentialKeyId,
|
|
901
|
+
verifierSecret,
|
|
902
|
+
operationFamilies,
|
|
903
|
+
requestId,
|
|
904
|
+
expiresAt,
|
|
905
|
+
...rotation.state === "active" ? { rotation } : {}
|
|
906
|
+
}, resolvedDeps);
|
|
907
|
+
} catch (error) {
|
|
908
|
+
await withUpstreamEnrollmentLock(paths, backendId, () => {
|
|
909
|
+
const failedStore = readStore(paths, resolvedDeps);
|
|
910
|
+
let current = latestEnrollment(failedStore, backendId);
|
|
911
|
+
if (!sameEnrollmentIdentity(current, staged.record))
|
|
912
|
+
return;
|
|
913
|
+
const abortDecision = decideUpstreamEnrollmentTransaction(current.transactionState, { type: "cancel" });
|
|
914
|
+
const aborting = abortDecision.next;
|
|
915
|
+
current = {
|
|
916
|
+
...current,
|
|
917
|
+
transactionState: aborting,
|
|
918
|
+
updatedAt: resolvedDeps.now().toISOString(),
|
|
919
|
+
failureReason: "challenge_creation_failed",
|
|
920
|
+
failureMessage: error instanceof Error ? error.message.slice(0, 240) : "Failed to create upstream enrollment challenge."
|
|
921
|
+
};
|
|
922
|
+
failedStore.enrollments = failedStore.enrollments.map((entry) => entry.backendId === current.backendId && entry.requestId === current.requestId ? current : entry);
|
|
923
|
+
failedStore.updatedAt = current.updatedAt;
|
|
924
|
+
writeStore(paths, failedStore, resolvedDeps);
|
|
925
|
+
let aborted;
|
|
926
|
+
try {
|
|
927
|
+
aborted = executeUpstreamEnrollmentTransactionEffect(abortDecision, {
|
|
928
|
+
abort_pending: () => abortPendingEnrollmentCredential(paths, backendId, current, resolvedDeps)
|
|
929
|
+
});
|
|
930
|
+
} catch (abortError) {
|
|
931
|
+
const recoveryRequired = {
|
|
932
|
+
...current,
|
|
933
|
+
transactionState: decideUpstreamEnrollmentTransaction(aborting, {
|
|
934
|
+
type: "effect_failed"
|
|
935
|
+
}).next,
|
|
936
|
+
updatedAt: resolvedDeps.now().toISOString(),
|
|
937
|
+
failureReason: "challenge_abort_interrupted",
|
|
938
|
+
failureMessage: abortError instanceof Error ? abortError.message.slice(0, 240) : "Challenge cleanup was interrupted."
|
|
939
|
+
};
|
|
940
|
+
failedStore.enrollments = failedStore.enrollments.map((entry) => entry.backendId === current.backendId && entry.requestId === current.requestId ? recoveryRequired : entry);
|
|
941
|
+
failedStore.updatedAt = recoveryRequired.updatedAt;
|
|
942
|
+
writeStore(paths, failedStore, resolvedDeps);
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
945
|
+
const transactionState = {
|
|
946
|
+
...decideUpstreamEnrollmentTransaction(aborting, {
|
|
947
|
+
type: "effect_succeeded"
|
|
948
|
+
}).next,
|
|
949
|
+
state: "failed"
|
|
950
|
+
};
|
|
951
|
+
const failed = {
|
|
952
|
+
...current,
|
|
953
|
+
state: "failed",
|
|
954
|
+
activationUrl: null,
|
|
955
|
+
updatedAt: resolvedDeps.now().toISOString(),
|
|
956
|
+
transactionState,
|
|
957
|
+
credential: aborted.credential,
|
|
958
|
+
failureReason: "challenge_creation_failed",
|
|
959
|
+
failureMessage: current.failureMessage
|
|
960
|
+
};
|
|
961
|
+
delete failed.pendingCredentialReference;
|
|
962
|
+
failedStore.enrollments = failedStore.enrollments.map((entry) => entry.backendId === current.backendId && entry.requestId === current.requestId ? failed : entry);
|
|
963
|
+
failedStore.updatedAt = failed.updatedAt;
|
|
964
|
+
writeStore(paths, failedStore, resolvedDeps);
|
|
965
|
+
});
|
|
966
|
+
return {
|
|
967
|
+
ok: false,
|
|
968
|
+
state: "failed",
|
|
969
|
+
backend: backendById(paths, backendId, resolvedDeps).backend,
|
|
970
|
+
message: error instanceof Error ? error.message : "Failed to create upstream enrollment challenge."
|
|
971
|
+
};
|
|
972
|
+
}
|
|
973
|
+
return withUpstreamEnrollmentLock(paths, backendId, () => {
|
|
974
|
+
const currentStore = readStore(paths, resolvedDeps);
|
|
975
|
+
const current = latestEnrollment(currentStore, backendId);
|
|
976
|
+
const currentBackend = backendById(paths, backendId, resolvedDeps).backend;
|
|
977
|
+
if (!sameEnrollmentIdentity(current, staged.record)) {
|
|
978
|
+
return enrollmentResultFromSnapshot(backendId, current, currentBackend);
|
|
979
|
+
}
|
|
980
|
+
const challengeDecision = decideUpstreamEnrollmentTransaction(current.transactionState, { type: "challenge_created" });
|
|
981
|
+
const pending = executeUpstreamEnrollmentTransactionEffect(challengeDecision, {
|
|
982
|
+
record_challenge: () => ({
|
|
983
|
+
...current,
|
|
984
|
+
challengeId: challenge.challengeId,
|
|
985
|
+
activationUrl: challenge.activationUrl,
|
|
986
|
+
transactionState: decideUpstreamEnrollmentTransaction(challengeDecision.next, { type: "effect_succeeded" }).next,
|
|
987
|
+
updatedAt: resolvedDeps.now().toISOString()
|
|
988
|
+
})
|
|
989
|
+
});
|
|
990
|
+
currentStore.enrollments = currentStore.enrollments.map((entry) => entry.backendId === current.backendId && entry.requestId === current.requestId ? pending : entry);
|
|
991
|
+
currentStore.updatedAt = pending.updatedAt;
|
|
992
|
+
writeStore(paths, currentStore, resolvedDeps);
|
|
993
|
+
return {
|
|
994
|
+
ok: true,
|
|
995
|
+
state: "pending",
|
|
996
|
+
backend: currentBackend,
|
|
997
|
+
enrollment: summarizeEnrollment(pending, currentBackend),
|
|
998
|
+
message: `Started upstream enrollment for ${backendId}. Open the activation URL to approve this local edge.`
|
|
999
|
+
};
|
|
1000
|
+
});
|
|
1001
|
+
};
|
|
1002
|
+
var startUpstreamEnrollment = (paths, id, deps = {}) => withSecureFetchForBackend(paths, id, deps, (resolvedDeps) => startUpstreamEnrollmentWithFetch(paths, id, resolvedDeps));
|
|
1003
|
+
var abortPendingEnrollmentCredential = (paths, backendId, record, deps) => {
|
|
1004
|
+
const replacement = replacementTransaction(record);
|
|
1005
|
+
deps.beforeEnrollmentEffect("abort_delete_pending_credential");
|
|
1006
|
+
deleteUpstreamCredentialSecret(paths.koedHome, record.credentialReference);
|
|
1007
|
+
if (replacement && replacement.predecessorCredentialReference !== record.credentialReference) {
|
|
1008
|
+
deps.beforeEnrollmentEffect("abort_restore_predecessor_registry");
|
|
1009
|
+
updateUpstreamBackendCredential(paths, backendId, {
|
|
1010
|
+
status: "configured",
|
|
1011
|
+
reference: replacement.predecessorCredentialReference
|
|
1012
|
+
}, {
|
|
1013
|
+
existsSync: deps.existsSync,
|
|
1014
|
+
readFileSync: deps.readFileSync,
|
|
1015
|
+
writeFileSync: deps.writeFileSync,
|
|
1016
|
+
renameSync: deps.renameSync,
|
|
1017
|
+
now: deps.now
|
|
1018
|
+
});
|
|
1019
|
+
return {
|
|
1020
|
+
credential: {
|
|
1021
|
+
status: "configured",
|
|
1022
|
+
reference: replacement.predecessorCredentialReference
|
|
1023
|
+
},
|
|
1024
|
+
predecessorRestored: true
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
deps.beforeEnrollmentEffect("abort_delete_local_client_credential");
|
|
1028
|
+
deleteLocalEdgeClientCredential(paths.koedHome, backendId);
|
|
1029
|
+
deps.beforeEnrollmentEffect("abort_clear_registry_credential");
|
|
1030
|
+
updateUpstreamBackendCredential(paths, backendId, { status: "not_configured" }, {
|
|
1031
|
+
existsSync: deps.existsSync,
|
|
1032
|
+
readFileSync: deps.readFileSync,
|
|
1033
|
+
writeFileSync: deps.writeFileSync,
|
|
1034
|
+
renameSync: deps.renameSync,
|
|
1035
|
+
now: deps.now
|
|
1036
|
+
});
|
|
1037
|
+
return {
|
|
1038
|
+
credential: { status: "not_configured" },
|
|
1039
|
+
predecessorRestored: false
|
|
1040
|
+
};
|
|
1041
|
+
};
|
|
1042
|
+
var getUpstreamEnrollmentStatusWithFetch = async (paths, id, deps = {}) => {
|
|
1043
|
+
const resolvedDeps = depsWithDefaults(deps);
|
|
1044
|
+
const backendId = validateBackendId(id);
|
|
1045
|
+
const identity = await ensureDeviceIdentity(paths);
|
|
1046
|
+
if (!identity.remoteOperationsAllowed || !identity.deviceInstanceId) {
|
|
1047
|
+
return {
|
|
1048
|
+
ok: false,
|
|
1049
|
+
state: "failed",
|
|
1050
|
+
message: `Local device identity is ${identity.health}; upstream enrollment is blocked until explicit identity rotation.`
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
const now = resolvedDeps.now();
|
|
1054
|
+
const { backend } = backendById(paths, backendId, resolvedDeps);
|
|
1055
|
+
const store = readStore(paths, resolvedDeps);
|
|
1056
|
+
const record = latestEnrollment(store, backendId);
|
|
1057
|
+
if (!record) {
|
|
1058
|
+
return {
|
|
1059
|
+
ok: true,
|
|
1060
|
+
state: "missing",
|
|
1061
|
+
backend,
|
|
1062
|
+
message: `No upstream enrollment has been started for ${backendId}.`
|
|
1063
|
+
};
|
|
1064
|
+
}
|
|
1065
|
+
if (record.transactionState.phase === "recovery_required" && record.transactionState.pendingEffect === "stage_pending_custody") {
|
|
1066
|
+
return withUpstreamEnrollmentLock(paths, backendId, () => {
|
|
1067
|
+
const recoveryStore = readStore(paths, resolvedDeps);
|
|
1068
|
+
const current = latestEnrollment(recoveryStore, backendId);
|
|
1069
|
+
const currentBackend = backendById(paths, backendId, resolvedDeps).backend;
|
|
1070
|
+
if (!sameEnrollmentIdentity(current, record)) {
|
|
1071
|
+
return enrollmentResultFromSnapshot(backendId, current, currentBackend);
|
|
1072
|
+
}
|
|
1073
|
+
const recoveryDecision = decideUpstreamEnrollmentTransaction(current.transactionState, { type: "recover" });
|
|
1074
|
+
const aborted = executeUpstreamEnrollmentTransactionEffect(recoveryDecision, {
|
|
1075
|
+
compensate_pending_custody: () => abortPendingEnrollmentCredential(paths, backendId, current, resolvedDeps)
|
|
1076
|
+
});
|
|
1077
|
+
const recovered = {
|
|
1078
|
+
...current,
|
|
1079
|
+
state: "failed",
|
|
1080
|
+
activationUrl: null,
|
|
1081
|
+
updatedAt: resolvedDeps.now().toISOString(),
|
|
1082
|
+
transactionState: decideUpstreamEnrollmentTransaction(recoveryDecision.next, { type: "effect_succeeded" }).next,
|
|
1083
|
+
credential: aborted.credential,
|
|
1084
|
+
failureReason: "prepared_effect_compensated",
|
|
1085
|
+
failureMessage: "Interrupted enrollment preparation was compensated; start enrollment again."
|
|
1086
|
+
};
|
|
1087
|
+
delete recovered.pendingCredentialReference;
|
|
1088
|
+
recoveryStore.enrollments = recoveryStore.enrollments.map((entry) => entry.backendId === current.backendId && entry.requestId === current.requestId ? recovered : entry);
|
|
1089
|
+
recoveryStore.updatedAt = recovered.updatedAt;
|
|
1090
|
+
writeStore(paths, recoveryStore, resolvedDeps);
|
|
1091
|
+
return {
|
|
1092
|
+
ok: false,
|
|
1093
|
+
state: "failed",
|
|
1094
|
+
backend: backendById(paths, backendId, resolvedDeps).backend,
|
|
1095
|
+
enrollment: summarizeEnrollment(recovered, backendById(paths, backendId, resolvedDeps).backend),
|
|
1096
|
+
message: recovered.failureMessage
|
|
1097
|
+
};
|
|
1098
|
+
});
|
|
1099
|
+
}
|
|
1100
|
+
if ((record.transactionState.phase === "aborting" || record.transactionState.phase === "recovery_required") && record.transactionState.pendingEffect === "abort_pending") {
|
|
1101
|
+
return withUpstreamEnrollmentLock(paths, backendId, () => {
|
|
1102
|
+
const recoveryStore = readStore(paths, resolvedDeps);
|
|
1103
|
+
const current = latestEnrollment(recoveryStore, backendId);
|
|
1104
|
+
let currentBackend = backendById(paths, backendId, resolvedDeps).backend;
|
|
1105
|
+
if (!sameEnrollmentIdentity(current, record)) {
|
|
1106
|
+
return enrollmentResultFromSnapshot(backendId, current, currentBackend);
|
|
1107
|
+
}
|
|
1108
|
+
const replacement = replacementTransaction(current);
|
|
1109
|
+
const resumed = decideUpstreamEnrollmentTransaction(current.transactionState, { type: "recover" });
|
|
1110
|
+
const aborted = executeUpstreamEnrollmentTransactionEffect(resumed, {
|
|
1111
|
+
abort_pending: () => abortPendingEnrollmentCredential(paths, backendId, current, resolvedDeps)
|
|
1112
|
+
});
|
|
1113
|
+
const recovered = {
|
|
1114
|
+
...current,
|
|
1115
|
+
state: current.transactionState.state,
|
|
1116
|
+
activationUrl: null,
|
|
1117
|
+
updatedAt: resolvedDeps.now().toISOString(),
|
|
1118
|
+
transactionState: decideUpstreamEnrollmentTransaction(resumed.next, {
|
|
1119
|
+
type: "effect_succeeded"
|
|
1120
|
+
}).next,
|
|
1121
|
+
credential: aborted.credential,
|
|
1122
|
+
...aborted.predecessorRestored && replacement ? {
|
|
1123
|
+
credentialReference: replacement.predecessorCredentialReference,
|
|
1124
|
+
activeCredentialReference: replacement.predecessorCredentialReference
|
|
1125
|
+
} : {}
|
|
1126
|
+
};
|
|
1127
|
+
delete recovered.pendingCredentialReference;
|
|
1128
|
+
recoveryStore.enrollments = recoveryStore.enrollments.map((entry) => entry.backendId === current.backendId && entry.requestId === current.requestId ? recovered : entry);
|
|
1129
|
+
recoveryStore.updatedAt = recovered.updatedAt;
|
|
1130
|
+
writeStore(paths, recoveryStore, resolvedDeps);
|
|
1131
|
+
currentBackend = backendById(paths, backendId, resolvedDeps).backend;
|
|
1132
|
+
return {
|
|
1133
|
+
ok: true,
|
|
1134
|
+
state: recovered.state,
|
|
1135
|
+
backend: currentBackend,
|
|
1136
|
+
enrollment: summarizeEnrollment(recovered, currentBackend),
|
|
1137
|
+
message: `Upstream enrollment for ${backendId} is ${recovered.state}.`
|
|
1138
|
+
};
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
const materialized = materializeState(record, now, backend);
|
|
1142
|
+
let credentialStatus = null;
|
|
1143
|
+
let upstreamStatus = null;
|
|
1144
|
+
if (backend && enrollmentCanReceiveRemoteStatus(materialized) && materialized.credentialReference) {
|
|
1145
|
+
credentialStatus = await remoteCredentialStatus(paths, backend, materialized.credentialReference, resolvedDeps);
|
|
1146
|
+
if (credentialStatus?.state !== "active" && !(materialized.state === "exchanged" && (credentialStatus?.state === "rejected" || credentialStatus?.state === "unknown")) && materialized.challengeId) {
|
|
1147
|
+
upstreamStatus = await readUpstreamChallengeStatus(backend, materialized.challengeId, identity.deviceInstanceId, resolvedDeps);
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
return withUpstreamEnrollmentLock(paths, backendId, () => {
|
|
1151
|
+
const currentStore = readStore(paths, resolvedDeps);
|
|
1152
|
+
const currentRecord = latestEnrollment(currentStore, backendId);
|
|
1153
|
+
let currentBackend = backendById(paths, backendId, resolvedDeps).backend;
|
|
1154
|
+
if (!sameEnrollmentIdentity(currentRecord, record)) {
|
|
1155
|
+
return enrollmentResultFromSnapshot(backendId, currentRecord, currentBackend);
|
|
1156
|
+
}
|
|
1157
|
+
let current = materializeState(currentRecord, now, currentBackend);
|
|
1158
|
+
let temporaryCredentialStatusFailure = false;
|
|
1159
|
+
const recordReplacement = replacementTransaction(record);
|
|
1160
|
+
const currentReplacement = replacementTransaction(current);
|
|
1161
|
+
const remoteResultStillApplies = currentBackend && enrollmentCanReceiveRemoteStatus(current) && current.credentialReference === record.credentialReference && currentBackend.credential.status !== "revoked" && currentBackend.credential.status !== "not_configured" && (currentBackend.credential.reference === record.credentialReference || recordReplacement !== null && currentBackend.credential.reference === recordReplacement.predecessorCredentialReference);
|
|
1162
|
+
if (remoteResultStillApplies && credentialStatus?.state === "active") {
|
|
1163
|
+
const commitDecision = decideUpstreamEnrollmentTransaction(current.transactionState, { type: "credential_observed", status: "active" });
|
|
1164
|
+
current = {
|
|
1165
|
+
...current,
|
|
1166
|
+
transactionState: commitDecision.next,
|
|
1167
|
+
updatedAt: now.toISOString()
|
|
1168
|
+
};
|
|
1169
|
+
currentStore.enrollments = currentStore.enrollments.map((entry) => entry.backendId === current.backendId && entry.requestId === current.requestId ? current : entry);
|
|
1170
|
+
currentStore.updatedAt = current.updatedAt;
|
|
1171
|
+
writeStore(paths, currentStore, resolvedDeps);
|
|
1172
|
+
try {
|
|
1173
|
+
executeUpstreamEnrollmentTransactionEffect(commitDecision, {
|
|
1174
|
+
commit_successor: () => {
|
|
1175
|
+
if (currentReplacement) {
|
|
1176
|
+
const localFamilies = localClientOperationFamiliesFor(current.requestedOperationFamilies);
|
|
1177
|
+
if (localFamilies.length > 0) {
|
|
1178
|
+
resolvedDeps.beforeEnrollmentEffect("commit_local_client_credential");
|
|
1179
|
+
const localClient = storeLocalEdgeClientCredential(paths.koedHome, {
|
|
1180
|
+
backendId,
|
|
1181
|
+
secret: randomSecret(resolvedDeps),
|
|
1182
|
+
operationFamilies: localFamilies
|
|
1183
|
+
});
|
|
1184
|
+
current = {
|
|
1185
|
+
...current,
|
|
1186
|
+
localClientCredentialReference: localClient.reference
|
|
1187
|
+
};
|
|
1188
|
+
} else {
|
|
1189
|
+
resolvedDeps.beforeEnrollmentEffect("commit_delete_local_client_credential");
|
|
1190
|
+
deleteLocalEdgeClientCredential(paths.koedHome, backendId);
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
resolvedDeps.beforeEnrollmentEffect("commit_registry_credential");
|
|
1194
|
+
updateUpstreamBackendCredential(paths, backendId, {
|
|
1195
|
+
status: "configured",
|
|
1196
|
+
reference: current.credentialReference
|
|
1197
|
+
}, {
|
|
1198
|
+
existsSync: resolvedDeps.existsSync,
|
|
1199
|
+
readFileSync: resolvedDeps.readFileSync,
|
|
1200
|
+
writeFileSync: resolvedDeps.writeFileSync,
|
|
1201
|
+
renameSync: resolvedDeps.renameSync,
|
|
1202
|
+
now: resolvedDeps.now
|
|
1203
|
+
});
|
|
1204
|
+
if (current.requestedOperationFamilies.includes("memory_answer_execution")) {
|
|
1205
|
+
resolvedDeps.beforeEnrollmentEffect("commit_memory_answer_route_policy");
|
|
1206
|
+
updateUpstreamBackendRoutePolicy(paths, backendId, { memoryAnswerExecution: "enabled" }, {
|
|
1207
|
+
existsSync: resolvedDeps.existsSync,
|
|
1208
|
+
readFileSync: resolvedDeps.readFileSync,
|
|
1209
|
+
writeFileSync: resolvedDeps.writeFileSync,
|
|
1210
|
+
renameSync: resolvedDeps.renameSync,
|
|
1211
|
+
now: resolvedDeps.now
|
|
1212
|
+
});
|
|
1213
|
+
}
|
|
1214
|
+
if (currentReplacement && currentReplacement.predecessorCredentialReference !== current.credentialReference) {
|
|
1215
|
+
resolvedDeps.beforeEnrollmentEffect("commit_delete_predecessor_credential");
|
|
1216
|
+
deleteUpstreamCredentialSecret(paths.koedHome, currentReplacement.predecessorCredentialReference);
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
});
|
|
1220
|
+
} catch (error) {
|
|
1221
|
+
current = {
|
|
1222
|
+
...current,
|
|
1223
|
+
transactionState: decideUpstreamEnrollmentTransaction(commitDecision.next, { type: "effect_failed" }).next,
|
|
1224
|
+
updatedAt: now.toISOString(),
|
|
1225
|
+
failureReason: "commit_effect_interrupted",
|
|
1226
|
+
failureMessage: error instanceof Error ? error.message.slice(0, 240) : "Enrollment commit was interrupted."
|
|
1227
|
+
};
|
|
1228
|
+
currentStore.enrollments = currentStore.enrollments.map((entry) => entry.backendId === current.backendId && entry.requestId === current.requestId ? current : entry);
|
|
1229
|
+
currentStore.updatedAt = current.updatedAt;
|
|
1230
|
+
writeStore(paths, currentStore, resolvedDeps);
|
|
1231
|
+
currentBackend = backendById(paths, backendId, resolvedDeps).backend;
|
|
1232
|
+
return {
|
|
1233
|
+
ok: false,
|
|
1234
|
+
state: current.state,
|
|
1235
|
+
backend: currentBackend,
|
|
1236
|
+
enrollment: summarizeEnrollment(current, currentBackend),
|
|
1237
|
+
message: "Upstream enrollment commit was interrupted and will recover on retry."
|
|
1238
|
+
};
|
|
1239
|
+
}
|
|
1240
|
+
currentBackend = backendById(paths, backendId, resolvedDeps).backend;
|
|
1241
|
+
current = {
|
|
1242
|
+
...current,
|
|
1243
|
+
state: "exchanged",
|
|
1244
|
+
updatedAt: now.toISOString(),
|
|
1245
|
+
failureReason: void 0,
|
|
1246
|
+
failureMessage: void 0,
|
|
1247
|
+
transactionState: decideUpstreamEnrollmentTransaction(commitDecision.next, { type: "effect_succeeded" }).next,
|
|
1248
|
+
activeCredentialReference: current.credentialReference,
|
|
1249
|
+
credential: {
|
|
1250
|
+
status: "configured",
|
|
1251
|
+
reference: current.credentialReference
|
|
1252
|
+
},
|
|
1253
|
+
deviceCredentialId: credentialStatus.deviceCredentialId,
|
|
1254
|
+
principalUserId: credentialStatus.principalUserId
|
|
1255
|
+
};
|
|
1256
|
+
delete current.pendingCredentialReference;
|
|
1257
|
+
} else if (remoteResultStillApplies && credentialStatus?.state === "rejected" && current.state === "exchanged") {
|
|
1258
|
+
const revokeDecision = decideUpstreamEnrollmentTransaction(current.transactionState, { type: "credential_observed", status: "rejected" });
|
|
1259
|
+
current = {
|
|
1260
|
+
...current,
|
|
1261
|
+
transactionState: revokeDecision.next,
|
|
1262
|
+
updatedAt: now.toISOString()
|
|
1263
|
+
};
|
|
1264
|
+
currentStore.enrollments = currentStore.enrollments.map((entry) => entry.backendId === current.backendId && entry.requestId === current.requestId ? current : entry);
|
|
1265
|
+
currentStore.updatedAt = current.updatedAt;
|
|
1266
|
+
writeStore(paths, currentStore, resolvedDeps);
|
|
1267
|
+
executeUpstreamEnrollmentTransactionEffect(revokeDecision, {
|
|
1268
|
+
revoke_active: () => {
|
|
1269
|
+
deleteUpstreamCredentialSecret(paths.koedHome, current.credentialReference);
|
|
1270
|
+
deleteLocalEdgeClientCredential(paths.koedHome, backendId);
|
|
1271
|
+
updateUpstreamBackendCredential(paths, backendId, { status: "not_configured" }, {
|
|
1272
|
+
existsSync: resolvedDeps.existsSync,
|
|
1273
|
+
readFileSync: resolvedDeps.readFileSync,
|
|
1274
|
+
writeFileSync: resolvedDeps.writeFileSync,
|
|
1275
|
+
renameSync: resolvedDeps.renameSync,
|
|
1276
|
+
now: resolvedDeps.now
|
|
1277
|
+
});
|
|
1278
|
+
}
|
|
1279
|
+
});
|
|
1280
|
+
currentBackend = backendById(paths, backendId, resolvedDeps).backend;
|
|
1281
|
+
current = {
|
|
1282
|
+
...current,
|
|
1283
|
+
state: "failed",
|
|
1284
|
+
updatedAt: now.toISOString(),
|
|
1285
|
+
transactionState: decideUpstreamEnrollmentTransaction(revokeDecision.next, { type: "effect_succeeded" }).next,
|
|
1286
|
+
failureReason: "credential_rejected",
|
|
1287
|
+
failureMessage: "Upstream backend rejected the stored device credential; restart enrollment.",
|
|
1288
|
+
credential: { status: "not_configured" }
|
|
1289
|
+
};
|
|
1290
|
+
delete current.activeCredentialReference;
|
|
1291
|
+
} else if (remoteResultStillApplies && credentialStatus?.state === "unknown" && current.state === "exchanged") {
|
|
1292
|
+
temporaryCredentialStatusFailure = true;
|
|
1293
|
+
current = {
|
|
1294
|
+
...current,
|
|
1295
|
+
updatedAt: now.toISOString(),
|
|
1296
|
+
failureReason: "credential_status_unavailable",
|
|
1297
|
+
failureMessage: "Could not verify the upstream device credential. Stored credentials were kept; retry when the Team Backend is available."
|
|
1298
|
+
};
|
|
1299
|
+
} else if (remoteResultStillApplies) {
|
|
1300
|
+
if (upstreamStatus === "denied" || upstreamStatus === "expired") {
|
|
1301
|
+
const abortDecision = decideUpstreamEnrollmentTransaction(current.transactionState, { type: "challenge_observed", status: upstreamStatus });
|
|
1302
|
+
current = {
|
|
1303
|
+
...current,
|
|
1304
|
+
transactionState: abortDecision.next,
|
|
1305
|
+
updatedAt: now.toISOString()
|
|
1306
|
+
};
|
|
1307
|
+
currentStore.enrollments = currentStore.enrollments.map((entry) => entry.backendId === current.backendId && entry.requestId === current.requestId ? current : entry);
|
|
1308
|
+
currentStore.updatedAt = current.updatedAt;
|
|
1309
|
+
writeStore(paths, currentStore, resolvedDeps);
|
|
1310
|
+
const aborted = executeUpstreamEnrollmentTransactionEffect(abortDecision, {
|
|
1311
|
+
abort_pending: () => abortPendingEnrollmentCredential(paths, backendId, current, resolvedDeps)
|
|
1312
|
+
});
|
|
1313
|
+
currentBackend = backendById(paths, backendId, resolvedDeps).backend;
|
|
1314
|
+
current = {
|
|
1315
|
+
...current,
|
|
1316
|
+
state: upstreamStatus,
|
|
1317
|
+
updatedAt: now.toISOString(),
|
|
1318
|
+
transactionState: decideUpstreamEnrollmentTransaction(abortDecision.next, { type: "effect_succeeded" }).next,
|
|
1319
|
+
credential: aborted.credential,
|
|
1320
|
+
...aborted.predecessorRestored ? {
|
|
1321
|
+
credentialReference: currentReplacement.predecessorCredentialReference,
|
|
1322
|
+
activeCredentialReference: currentReplacement.predecessorCredentialReference
|
|
1323
|
+
} : {}
|
|
1324
|
+
};
|
|
1325
|
+
delete current.pendingCredentialReference;
|
|
1326
|
+
} else if (upstreamStatus === "approved") {
|
|
1327
|
+
const approval = decideUpstreamEnrollmentTransaction(current.transactionState, { type: "challenge_observed", status: "approved" });
|
|
1328
|
+
current = {
|
|
1329
|
+
...current,
|
|
1330
|
+
state: "approved",
|
|
1331
|
+
transactionState: approval.next,
|
|
1332
|
+
updatedAt: now.toISOString()
|
|
1333
|
+
};
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
if (current.state === "expired" && current.credential?.status !== "configured" && current.transactionState.phase !== "aborted") {
|
|
1337
|
+
const expiringReplacement = replacementTransaction(current);
|
|
1338
|
+
const abortDecision = decideUpstreamEnrollmentTransaction(current.transactionState, { type: "challenge_observed", status: "expired" });
|
|
1339
|
+
current = {
|
|
1340
|
+
...current,
|
|
1341
|
+
transactionState: abortDecision.next,
|
|
1342
|
+
updatedAt: now.toISOString()
|
|
1343
|
+
};
|
|
1344
|
+
currentStore.enrollments = currentStore.enrollments.map((entry) => entry.backendId === current.backendId && entry.requestId === current.requestId ? current : entry);
|
|
1345
|
+
currentStore.updatedAt = current.updatedAt;
|
|
1346
|
+
writeStore(paths, currentStore, resolvedDeps);
|
|
1347
|
+
const aborted = executeUpstreamEnrollmentTransactionEffect(abortDecision, {
|
|
1348
|
+
abort_pending: () => abortPendingEnrollmentCredential(paths, backendId, current, resolvedDeps)
|
|
1349
|
+
});
|
|
1350
|
+
currentBackend = backendById(paths, backendId, resolvedDeps).backend;
|
|
1351
|
+
current = {
|
|
1352
|
+
...current,
|
|
1353
|
+
transactionState: decideUpstreamEnrollmentTransaction(abortDecision.next, { type: "effect_succeeded" }).next,
|
|
1354
|
+
credential: aborted.credential,
|
|
1355
|
+
...aborted.predecessorRestored ? {
|
|
1356
|
+
credentialReference: expiringReplacement.predecessorCredentialReference,
|
|
1357
|
+
activeCredentialReference: expiringReplacement.predecessorCredentialReference
|
|
1358
|
+
} : {}
|
|
1359
|
+
};
|
|
1360
|
+
delete current.pendingCredentialReference;
|
|
1361
|
+
}
|
|
1362
|
+
if (JSON.stringify(current) !== JSON.stringify(currentRecord)) {
|
|
1363
|
+
currentStore.enrollments = currentStore.enrollments.map((entry) => entry.backendId === currentRecord.backendId && entry.requestId === currentRecord.requestId ? current : entry);
|
|
1364
|
+
currentStore.updatedAt = now.toISOString();
|
|
1365
|
+
resolvedDeps.beforeEnrollmentEffect("commit_enrollment_state");
|
|
1366
|
+
writeStore(paths, currentStore, resolvedDeps);
|
|
1367
|
+
}
|
|
1368
|
+
return {
|
|
1369
|
+
ok: !temporaryCredentialStatusFailure,
|
|
1370
|
+
state: current.state,
|
|
1371
|
+
backend: currentBackend,
|
|
1372
|
+
enrollment: summarizeEnrollment(current, currentBackend),
|
|
1373
|
+
message: temporaryCredentialStatusFailure ? current.failureMessage : `Upstream enrollment for ${backendId} is ${current.state}.`
|
|
1374
|
+
};
|
|
1375
|
+
});
|
|
1376
|
+
};
|
|
1377
|
+
var getUpstreamEnrollmentStatus = (paths, id, deps = {}) => withSecureFetchForBackend(paths, id, deps, (resolvedDeps) => getUpstreamEnrollmentStatusWithFetch(paths, id, resolvedDeps));
|
|
1378
|
+
var cancelUpstreamEnrollment = async (paths, id, deps = {}) => {
|
|
1379
|
+
const resolvedDeps = depsWithDefaults(deps);
|
|
1380
|
+
const backendId = validateBackendId(id);
|
|
1381
|
+
return withUpstreamEnrollmentLock(paths, backendId, () => {
|
|
1382
|
+
const nowDate = resolvedDeps.now();
|
|
1383
|
+
const now = nowDate.toISOString();
|
|
1384
|
+
const { backend } = backendById(paths, backendId, resolvedDeps);
|
|
1385
|
+
const store = readStore(paths, resolvedDeps);
|
|
1386
|
+
const record = latestEnrollment(store, backendId);
|
|
1387
|
+
if (!record) {
|
|
1388
|
+
return {
|
|
1389
|
+
ok: true,
|
|
1390
|
+
state: "missing",
|
|
1391
|
+
backend,
|
|
1392
|
+
message: `No upstream enrollment has been started for ${backendId}.`
|
|
1393
|
+
};
|
|
1394
|
+
}
|
|
1395
|
+
const materialized = materializeState(record, nowDate, backend);
|
|
1396
|
+
if (materialized.transactionState.pendingEffect === "commit_successor") {
|
|
1397
|
+
return {
|
|
1398
|
+
ok: false,
|
|
1399
|
+
state: materialized.state,
|
|
1400
|
+
backend,
|
|
1401
|
+
enrollment: summarizeEnrollment(materialized, backend),
|
|
1402
|
+
message: "The successor credential is already active and its local commit must recover before enrollment can be canceled."
|
|
1403
|
+
};
|
|
1404
|
+
}
|
|
1405
|
+
if (materialized.state === "exchanged" || materialized.state === "revoked" || materialized.state === "expired" || materialized.state === "failed" || materialized.state === "denied") {
|
|
1406
|
+
return {
|
|
1407
|
+
ok: true,
|
|
1408
|
+
state: materialized.state,
|
|
1409
|
+
backend,
|
|
1410
|
+
enrollment: summarizeEnrollment(materialized, backend),
|
|
1411
|
+
message: `Upstream enrollment for ${backendId} is already ${materialized.state}.`
|
|
1412
|
+
};
|
|
1413
|
+
}
|
|
1414
|
+
const abortDecision = decideUpstreamEnrollmentTransaction(materialized.transactionState, { type: "cancel" });
|
|
1415
|
+
const aborting = {
|
|
1416
|
+
...materialized,
|
|
1417
|
+
transactionState: abortDecision.next,
|
|
1418
|
+
updatedAt: now
|
|
1419
|
+
};
|
|
1420
|
+
store.enrollments = store.enrollments.map((entry) => entry.backendId === record.backendId && entry.requestId === record.requestId ? aborting : entry);
|
|
1421
|
+
store.updatedAt = now;
|
|
1422
|
+
writeStore(paths, store, resolvedDeps);
|
|
1423
|
+
const aborted = executeUpstreamEnrollmentTransactionEffect(abortDecision, {
|
|
1424
|
+
abort_pending: () => abortPendingEnrollmentCredential(paths, backendId, aborting, resolvedDeps)
|
|
1425
|
+
});
|
|
1426
|
+
const canceledReplacement = replacementTransaction(aborting);
|
|
1427
|
+
const canceled = {
|
|
1428
|
+
...aborting,
|
|
1429
|
+
state: "canceled",
|
|
1430
|
+
updatedAt: now,
|
|
1431
|
+
transactionState: decideUpstreamEnrollmentTransaction(abortDecision.next, { type: "effect_succeeded" }).next,
|
|
1432
|
+
credential: aborted.credential,
|
|
1433
|
+
...aborted.predecessorRestored ? {
|
|
1434
|
+
credentialReference: canceledReplacement.predecessorCredentialReference,
|
|
1435
|
+
activeCredentialReference: canceledReplacement.predecessorCredentialReference
|
|
1436
|
+
} : {}
|
|
1437
|
+
};
|
|
1438
|
+
delete canceled.pendingCredentialReference;
|
|
1439
|
+
store.enrollments = store.enrollments.map((entry) => entry.backendId === record.backendId && entry.requestId === record.requestId ? canceled : entry);
|
|
1440
|
+
store.updatedAt = now;
|
|
1441
|
+
writeStore(paths, store, resolvedDeps);
|
|
1442
|
+
if (!aborted.predecessorRestored && getActiveUpstreamBackend(paths)?.id === backendId) {
|
|
1443
|
+
setActiveUpstreamBackend(paths, null, {
|
|
1444
|
+
existsSync: resolvedDeps.existsSync,
|
|
1445
|
+
readFileSync: resolvedDeps.readFileSync,
|
|
1446
|
+
writeFileSync: resolvedDeps.writeFileSync,
|
|
1447
|
+
renameSync: resolvedDeps.renameSync,
|
|
1448
|
+
now: resolvedDeps.now
|
|
1449
|
+
});
|
|
1450
|
+
}
|
|
1451
|
+
return {
|
|
1452
|
+
ok: true,
|
|
1453
|
+
state: "canceled",
|
|
1454
|
+
backend,
|
|
1455
|
+
enrollment: summarizeEnrollment(canceled, backend),
|
|
1456
|
+
message: `Canceled upstream enrollment for ${backendId}.`
|
|
1457
|
+
};
|
|
1458
|
+
});
|
|
1459
|
+
};
|
|
1460
|
+
var invalidateUpstreamEnrollmentReferences = async (paths, deps = {}) => {
|
|
1461
|
+
const resolvedDeps = depsWithDefaults(deps);
|
|
1462
|
+
const registry = collectUpstreamRegistryStatus(paths, {
|
|
1463
|
+
existsSync: resolvedDeps.existsSync,
|
|
1464
|
+
readFileSync: resolvedDeps.readFileSync,
|
|
1465
|
+
now: resolvedDeps.now
|
|
1466
|
+
});
|
|
1467
|
+
if (registry.parseError) {
|
|
1468
|
+
throw new Error("Upstream backend registry is malformed.");
|
|
1469
|
+
}
|
|
1470
|
+
let pendingRemoteRevocation = false;
|
|
1471
|
+
for (const backend of registry.backends) {
|
|
1472
|
+
await withUpstreamEnrollmentLock(paths, backend.id, () => {
|
|
1473
|
+
const now = resolvedDeps.now().toISOString();
|
|
1474
|
+
const current = backendById(paths, backend.id, resolvedDeps).backend;
|
|
1475
|
+
if (!current)
|
|
1476
|
+
return;
|
|
1477
|
+
if (current.credential.reference && current.credential.status !== "revoked" && current.credential.status !== "not_configured") {
|
|
1478
|
+
pendingRemoteRevocation = true;
|
|
1479
|
+
}
|
|
1480
|
+
updateUpstreamBackendRoutePolicy(paths, backend.id, {
|
|
1481
|
+
personalMemoryRead: "disabled",
|
|
1482
|
+
personalCollaboration: "disabled",
|
|
1483
|
+
teamWorkspaceRead: "disabled",
|
|
1484
|
+
memoryAnswerExecution: "disabled",
|
|
1485
|
+
shareGrantManagement: "disabled",
|
|
1486
|
+
captureWrites: "disabled",
|
|
1487
|
+
sync: "disabled",
|
|
1488
|
+
admin: "disabled"
|
|
1489
|
+
}, {
|
|
1490
|
+
existsSync: resolvedDeps.existsSync,
|
|
1491
|
+
readFileSync: resolvedDeps.readFileSync,
|
|
1492
|
+
writeFileSync: resolvedDeps.writeFileSync,
|
|
1493
|
+
renameSync: resolvedDeps.renameSync,
|
|
1494
|
+
now: resolvedDeps.now
|
|
1495
|
+
});
|
|
1496
|
+
updateUpstreamBackendCredential(paths, backend.id, { status: "revoked", reference: current.credential.reference }, {
|
|
1497
|
+
existsSync: resolvedDeps.existsSync,
|
|
1498
|
+
readFileSync: resolvedDeps.readFileSync,
|
|
1499
|
+
writeFileSync: resolvedDeps.writeFileSync,
|
|
1500
|
+
renameSync: resolvedDeps.renameSync,
|
|
1501
|
+
now: resolvedDeps.now
|
|
1502
|
+
});
|
|
1503
|
+
deleteUpstreamCredentialSecret(paths.koedHome, current.credential.reference);
|
|
1504
|
+
deleteLocalEdgeClientCredential(paths.koedHome, backend.id);
|
|
1505
|
+
const store = readStore(paths, resolvedDeps);
|
|
1506
|
+
store.enrollments = store.enrollments.map((record) => record.backendId === backend.id ? {
|
|
1507
|
+
...record,
|
|
1508
|
+
state: "revoked",
|
|
1509
|
+
updatedAt: now,
|
|
1510
|
+
credential: {
|
|
1511
|
+
status: "revoked",
|
|
1512
|
+
reference: record.credentialReference
|
|
1513
|
+
}
|
|
1514
|
+
} : record);
|
|
1515
|
+
store.updatedAt = now;
|
|
1516
|
+
writeStore(paths, store, resolvedDeps);
|
|
1517
|
+
});
|
|
1518
|
+
}
|
|
1519
|
+
return { pendingRemoteRevocation };
|
|
1520
|
+
};
|
|
1521
|
+
var disconnectUpstreamBackendEnrollmentWithFetch = async (paths, id, deps = {}) => {
|
|
1522
|
+
const resolvedDeps = depsWithDefaults(deps);
|
|
1523
|
+
const backendId = validateBackendId(id);
|
|
1524
|
+
return withUpstreamEnrollmentLock(paths, backendId, async () => {
|
|
1525
|
+
const now = resolvedDeps.now().toISOString();
|
|
1526
|
+
const { backend } = backendById(paths, backendId, resolvedDeps);
|
|
1527
|
+
if (!backend) {
|
|
1528
|
+
return {
|
|
1529
|
+
ok: false,
|
|
1530
|
+
state: "missing",
|
|
1531
|
+
message: `Upstream backend ${backendId} is not registered.`
|
|
1532
|
+
};
|
|
1533
|
+
}
|
|
1534
|
+
beginUpstreamDisconnectCleanup(paths, backendId, {
|
|
1535
|
+
now: resolvedDeps.now
|
|
1536
|
+
});
|
|
1537
|
+
if (backend.credential.status !== "revoked" && backend.credential.status !== "not_configured") {
|
|
1538
|
+
const remoteRevocation = await revokeRemoteCredential(paths, backend, backend.credential.reference, resolvedDeps);
|
|
1539
|
+
if (remoteRevocation === "unavailable") {
|
|
1540
|
+
updateUpstreamDisconnectCleanup(paths, backendId, {
|
|
1541
|
+
phase: "remote_revocation_pending",
|
|
1542
|
+
lastFailureCategory: "remote_unavailable"
|
|
1543
|
+
}, { now: resolvedDeps.now });
|
|
1544
|
+
return {
|
|
1545
|
+
ok: false,
|
|
1546
|
+
state: "failed",
|
|
1547
|
+
backend,
|
|
1548
|
+
message: "The upstream device credential could not be revoked. Local credentials and routes were kept so disconnect can be retried safely."
|
|
1549
|
+
};
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
updateUpstreamDisconnectCleanup(paths, backendId, {
|
|
1553
|
+
phase: "local_cleanup_pending",
|
|
1554
|
+
lastFailureCategory: null
|
|
1555
|
+
}, { now: resolvedDeps.now });
|
|
1556
|
+
updateUpstreamBackendRoutePolicy(paths, backendId, {
|
|
1557
|
+
personalMemoryRead: "disabled",
|
|
1558
|
+
personalCollaboration: "disabled",
|
|
1559
|
+
teamWorkspaceRead: "disabled",
|
|
1560
|
+
memoryAnswerExecution: "disabled",
|
|
1561
|
+
shareGrantManagement: "disabled",
|
|
1562
|
+
captureWrites: "disabled",
|
|
1563
|
+
sync: "disabled",
|
|
1564
|
+
admin: "disabled"
|
|
1565
|
+
}, {
|
|
1566
|
+
existsSync: resolvedDeps.existsSync,
|
|
1567
|
+
readFileSync: resolvedDeps.readFileSync,
|
|
1568
|
+
writeFileSync: resolvedDeps.writeFileSync,
|
|
1569
|
+
renameSync: resolvedDeps.renameSync,
|
|
1570
|
+
now: resolvedDeps.now
|
|
1571
|
+
});
|
|
1572
|
+
updateUpstreamBackendCredential(paths, backendId, { status: "revoked" }, {
|
|
1573
|
+
existsSync: resolvedDeps.existsSync,
|
|
1574
|
+
readFileSync: resolvedDeps.readFileSync,
|
|
1575
|
+
writeFileSync: resolvedDeps.writeFileSync,
|
|
1576
|
+
renameSync: resolvedDeps.renameSync,
|
|
1577
|
+
now: resolvedDeps.now
|
|
1578
|
+
});
|
|
1579
|
+
clearUpstreamBackendCapabilities(paths, backendId, {
|
|
1580
|
+
existsSync: resolvedDeps.existsSync,
|
|
1581
|
+
readFileSync: resolvedDeps.readFileSync,
|
|
1582
|
+
writeFileSync: resolvedDeps.writeFileSync,
|
|
1583
|
+
renameSync: resolvedDeps.renameSync,
|
|
1584
|
+
now: resolvedDeps.now
|
|
1585
|
+
});
|
|
1586
|
+
deleteUpstreamCredentialSecret(paths.koedHome, backend.credential.reference);
|
|
1587
|
+
deleteLocalEdgeClientCredential(paths.koedHome, backendId);
|
|
1588
|
+
const refreshedBackend = backendById(paths, backendId, resolvedDeps).backend;
|
|
1589
|
+
const store = readStore(paths, resolvedDeps);
|
|
1590
|
+
const existing = latestEnrollment(store, backendId);
|
|
1591
|
+
const record = {
|
|
1592
|
+
backendId,
|
|
1593
|
+
requestId: existing?.requestId ?? resolvedDeps.randomId(),
|
|
1594
|
+
state: "revoked",
|
|
1595
|
+
activationUrl: null,
|
|
1596
|
+
requestedOperationFamilies: [],
|
|
1597
|
+
transaction: { kind: "initial" },
|
|
1598
|
+
transactionState: {
|
|
1599
|
+
...createUpstreamEnrollmentTransaction({
|
|
1600
|
+
id: existing?.transactionState.id ?? existing?.requestId ?? "revoked",
|
|
1601
|
+
generation: existing?.transactionState.generation ?? 1,
|
|
1602
|
+
kind: "initial"
|
|
1603
|
+
}),
|
|
1604
|
+
phase: "aborted",
|
|
1605
|
+
state: "revoked",
|
|
1606
|
+
pendingEffect: null
|
|
1607
|
+
},
|
|
1608
|
+
updatedAt: now,
|
|
1609
|
+
createdAt: existing?.createdAt ?? now,
|
|
1610
|
+
expiresAt: null,
|
|
1611
|
+
credential: { status: "revoked" }
|
|
1612
|
+
};
|
|
1613
|
+
if (existing) {
|
|
1614
|
+
store.enrollments = store.enrollments.map((entry) => entry.backendId === existing.backendId && entry.requestId === existing.requestId ? record : entry);
|
|
1615
|
+
} else {
|
|
1616
|
+
store.enrollments.push(record);
|
|
1617
|
+
}
|
|
1618
|
+
store.updatedAt = now;
|
|
1619
|
+
writeStore(paths, store, resolvedDeps);
|
|
1620
|
+
if (getActiveUpstreamBackend(paths)?.id === backendId) {
|
|
1621
|
+
setActiveUpstreamBackend(paths, null, {
|
|
1622
|
+
existsSync: resolvedDeps.existsSync,
|
|
1623
|
+
readFileSync: resolvedDeps.readFileSync,
|
|
1624
|
+
writeFileSync: resolvedDeps.writeFileSync,
|
|
1625
|
+
renameSync: resolvedDeps.renameSync,
|
|
1626
|
+
now: resolvedDeps.now
|
|
1627
|
+
});
|
|
1628
|
+
}
|
|
1629
|
+
return {
|
|
1630
|
+
ok: true,
|
|
1631
|
+
state: "revoked",
|
|
1632
|
+
backend: refreshedBackend,
|
|
1633
|
+
enrollment: summarizeEnrollment(record, refreshedBackend),
|
|
1634
|
+
message: `Disconnected upstream backend ${backendId} and revoked its exchanged device credential.`
|
|
1635
|
+
};
|
|
1636
|
+
});
|
|
1637
|
+
};
|
|
1638
|
+
var disconnectUpstreamBackendEnrollment = (paths, id, deps = {}) => withSecureFetchForBackend(paths, id, deps, (resolvedDeps) => disconnectUpstreamBackendEnrollmentWithFetch(paths, id, resolvedDeps));
|
|
1639
|
+
|
|
1640
|
+
export {
|
|
1641
|
+
readUpstreamEnrollmentBinding,
|
|
1642
|
+
startUpstreamEnrollment,
|
|
1643
|
+
getUpstreamEnrollmentStatus,
|
|
1644
|
+
cancelUpstreamEnrollment,
|
|
1645
|
+
invalidateUpstreamEnrollmentReferences,
|
|
1646
|
+
disconnectUpstreamBackendEnrollment
|
|
1647
|
+
};
|