@4xeoz/re-entry 0.2.0
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/README.md +337 -0
- package/node_modules/@webmcp-challenge/reentry-core/README.md +112 -0
- package/node_modules/@webmcp-challenge/reentry-core/package.json +38 -0
- package/node_modules/@webmcp-challenge/reentry-core/protocol/test-vectors/v0.1.json +47 -0
- package/node_modules/@webmcp-challenge/reentry-core/src/agent-adapter.mjs +438 -0
- package/node_modules/@webmcp-challenge/reentry-core/src/cloud-receiver-http.mjs +268 -0
- package/node_modules/@webmcp-challenge/reentry-core/src/host-sdk.mjs +278 -0
- package/node_modules/@webmcp-challenge/reentry-core/src/index.mjs +3 -0
- package/node_modules/@webmcp-challenge/reentry-core/src/local-connector-client.mjs +530 -0
- package/node_modules/@webmcp-challenge/reentry-core/src/managed-context-adapter.mjs +275 -0
- package/node_modules/@webmcp-challenge/reentry-core/src/protocol.mjs +845 -0
- package/node_modules/@webmcp-challenge/reentry-core/src/receiver-core.mjs +867 -0
- package/node_modules/@webmcp-challenge/reentry-core/src/receiver-delivery.mjs +613 -0
- package/node_modules/@webmcp-challenge/reentry-core/src/receiver-http-contract.mjs +24 -0
- package/node_modules/@webmcp-challenge/reentry-core/src/receiver-support.mjs +151 -0
- package/node_modules/@webmcp-challenge/reentry-core/src/sqlite-receiver-schema.mjs +184 -0
- package/node_modules/@webmcp-challenge/reentry-core/src/sqlite-receiver-store.mjs +573 -0
- package/package.json +44 -0
- package/src/browser-prompt.mjs +18 -0
- package/src/codex-discovery.mjs +246 -0
- package/src/codex-exec-adapter.mjs +197 -0
- package/src/codex-queue-adapter.mjs +214 -0
- package/src/credentials.mjs +87 -0
- package/src/index.mjs +6 -0
- package/src/local-connector.mjs +82 -0
- package/src/macos-service.mjs +184 -0
- package/src/main.mjs +733 -0
- package/src/pairing-client.mjs +545 -0
- package/src/terminal-ui.mjs +99 -0
|
@@ -0,0 +1,867 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
ACCEPTANCE_TYPE,
|
|
5
|
+
PROTOCOL_VERSION,
|
|
6
|
+
PUBLIC_BINDING_TYPE,
|
|
7
|
+
canonicalJson,
|
|
8
|
+
createContinuationAcceptance,
|
|
9
|
+
createContinuationReceipt,
|
|
10
|
+
parseContinuationEventBody,
|
|
11
|
+
validatePublicBinding,
|
|
12
|
+
validateReentryManifest,
|
|
13
|
+
verifyContinuationEventEnvelope,
|
|
14
|
+
} from "./protocol.mjs";
|
|
15
|
+
import { ReceiverDelivery } from "./receiver-delivery.mjs";
|
|
16
|
+
import {
|
|
17
|
+
authorization,
|
|
18
|
+
conflict,
|
|
19
|
+
deepFreeze,
|
|
20
|
+
invariant,
|
|
21
|
+
notFound,
|
|
22
|
+
requireExactInput,
|
|
23
|
+
requireIdentifier,
|
|
24
|
+
requireOpaqueToken,
|
|
25
|
+
requireTimestamp,
|
|
26
|
+
scope,
|
|
27
|
+
} from "./receiver-support.mjs";
|
|
28
|
+
|
|
29
|
+
export {
|
|
30
|
+
CONNECTOR_IDENTITY_TYPE,
|
|
31
|
+
DELIVERY_ACKNOWLEDGEMENT_TYPE,
|
|
32
|
+
DELIVERY_LEASE_TYPE,
|
|
33
|
+
HOST_EFFECT_ATTESTATION_TYPE,
|
|
34
|
+
HOST_EFFECT_OUTCOME,
|
|
35
|
+
} from "./receiver-delivery.mjs";
|
|
36
|
+
export {
|
|
37
|
+
ReceiverAuthorizationError,
|
|
38
|
+
ReceiverConflictError,
|
|
39
|
+
ReceiverInvariantError,
|
|
40
|
+
ReceiverNotFoundError,
|
|
41
|
+
ReceiverScopeError,
|
|
42
|
+
ReceiverValidationError,
|
|
43
|
+
} from "./receiver-support.mjs";
|
|
44
|
+
|
|
45
|
+
export const CONSENT_DECISION_TYPE = "webmcp.receiver_consent_decision";
|
|
46
|
+
export const GRANT_CONTROL_AUTHORIZATION_TYPE =
|
|
47
|
+
"webmcp.receiver_grant_control_authorization";
|
|
48
|
+
export const GRANT_SUMMARY_TYPE = "webmcp.receiver_grant_summary";
|
|
49
|
+
export const GRANT_REVOCATION_TYPE = "webmcp.receiver_grant_revocation";
|
|
50
|
+
|
|
51
|
+
const RECEIVER_OPTION_FIELDS = Object.freeze([
|
|
52
|
+
"store",
|
|
53
|
+
"keyResolver",
|
|
54
|
+
"consentAuthority",
|
|
55
|
+
"grantControlAuthority",
|
|
56
|
+
"connectorAuthority",
|
|
57
|
+
"effectAuthority",
|
|
58
|
+
"maximumGrantLifetimeMs",
|
|
59
|
+
"leaseDurationMs",
|
|
60
|
+
"maximumDeliveryAttempts",
|
|
61
|
+
"clock",
|
|
62
|
+
"createId",
|
|
63
|
+
]);
|
|
64
|
+
const CREATE_CHALLENGE_FIELDS = Object.freeze(["manifest", "expectedOrigin"]);
|
|
65
|
+
const DECIDE_CONSENT_FIELDS = Object.freeze(["challengeId", "decisionToken"]);
|
|
66
|
+
const GRANT_CONTROL_INPUT_FIELDS = Object.freeze(["bindingId", "controlToken"]);
|
|
67
|
+
const ENVELOPE_FIELDS = Object.freeze(["body", "headers"]);
|
|
68
|
+
const APPROVAL_DECISION_FIELDS = Object.freeze([
|
|
69
|
+
"type",
|
|
70
|
+
"protocol_version",
|
|
71
|
+
"decision_id",
|
|
72
|
+
"challenge_id",
|
|
73
|
+
"action",
|
|
74
|
+
"subject_id",
|
|
75
|
+
"delivery_target_id",
|
|
76
|
+
"decided_at",
|
|
77
|
+
]);
|
|
78
|
+
const DECLINE_DECISION_FIELDS = Object.freeze(
|
|
79
|
+
APPROVAL_DECISION_FIELDS.filter((field) => field !== "delivery_target_id"),
|
|
80
|
+
);
|
|
81
|
+
const GRANT_CONTROL_AUTHORIZATION_FIELDS = Object.freeze([
|
|
82
|
+
"type",
|
|
83
|
+
"protocol_version",
|
|
84
|
+
"binding_id",
|
|
85
|
+
"action",
|
|
86
|
+
"subject_id",
|
|
87
|
+
"authenticated_at",
|
|
88
|
+
"expires_at",
|
|
89
|
+
]);
|
|
90
|
+
const STORE_METHODS = Object.freeze([
|
|
91
|
+
"transaction",
|
|
92
|
+
"getChallengeByManifestId",
|
|
93
|
+
"getChallengeById",
|
|
94
|
+
"insertChallenge",
|
|
95
|
+
"setChallengeDecision",
|
|
96
|
+
"getGrantByChallengeId",
|
|
97
|
+
"getGrantByBindingId",
|
|
98
|
+
"insertGrant",
|
|
99
|
+
"revokeGrant",
|
|
100
|
+
"consumeGrantRun",
|
|
101
|
+
"getEventById",
|
|
102
|
+
"insertEvent",
|
|
103
|
+
"insertDelivery",
|
|
104
|
+
]);
|
|
105
|
+
const AUTHORITY_FUTURE_SKEW_MS = 60 * 1_000;
|
|
106
|
+
const MIN_LEASE_DURATION_MS = 1_000;
|
|
107
|
+
const MAX_LEASE_DURATION_MS = 5 * 60 * 1_000;
|
|
108
|
+
const MAX_DELIVERY_ATTEMPTS_LIMIT = 100;
|
|
109
|
+
|
|
110
|
+
export class ReceiverCore {
|
|
111
|
+
#store;
|
|
112
|
+
#keyResolver;
|
|
113
|
+
#consentAuthority;
|
|
114
|
+
#grantControlAuthority;
|
|
115
|
+
#maximumGrantLifetimeMs;
|
|
116
|
+
#maximumDeliveryAttempts;
|
|
117
|
+
#clock;
|
|
118
|
+
#createId;
|
|
119
|
+
#delivery;
|
|
120
|
+
|
|
121
|
+
constructor(options) {
|
|
122
|
+
requireExactInput(
|
|
123
|
+
options,
|
|
124
|
+
RECEIVER_OPTION_FIELDS,
|
|
125
|
+
[
|
|
126
|
+
"store",
|
|
127
|
+
"keyResolver",
|
|
128
|
+
"consentAuthority",
|
|
129
|
+
"grantControlAuthority",
|
|
130
|
+
"connectorAuthority",
|
|
131
|
+
"effectAuthority",
|
|
132
|
+
"maximumGrantLifetimeMs",
|
|
133
|
+
"leaseDurationMs",
|
|
134
|
+
"maximumDeliveryAttempts",
|
|
135
|
+
],
|
|
136
|
+
"Receiver Core options",
|
|
137
|
+
);
|
|
138
|
+
requireStore(options.store);
|
|
139
|
+
if (typeof options.keyResolver !== "function") {
|
|
140
|
+
throw new TypeError("Receiver Core keyResolver must be a function");
|
|
141
|
+
}
|
|
142
|
+
if (typeof options.consentAuthority?.verifyDecision !== "function") {
|
|
143
|
+
throw new TypeError("Receiver Core consentAuthority must implement verifyDecision");
|
|
144
|
+
}
|
|
145
|
+
if (typeof options.grantControlAuthority?.verifyControl !== "function") {
|
|
146
|
+
throw new TypeError("Receiver Core grantControlAuthority must implement verifyControl");
|
|
147
|
+
}
|
|
148
|
+
if (typeof options.connectorAuthority?.verifyConnector !== "function") {
|
|
149
|
+
throw new TypeError("Receiver Core connectorAuthority must implement verifyConnector");
|
|
150
|
+
}
|
|
151
|
+
if (typeof options.effectAuthority?.verifyEffect !== "function") {
|
|
152
|
+
throw new TypeError("Receiver Core effectAuthority must implement verifyEffect");
|
|
153
|
+
}
|
|
154
|
+
if (
|
|
155
|
+
!Number.isSafeInteger(options.maximumGrantLifetimeMs) ||
|
|
156
|
+
options.maximumGrantLifetimeMs < 1_000
|
|
157
|
+
) {
|
|
158
|
+
throw new TypeError("Receiver Core maximumGrantLifetimeMs must be at least one second");
|
|
159
|
+
}
|
|
160
|
+
if (
|
|
161
|
+
!Number.isSafeInteger(options.leaseDurationMs) ||
|
|
162
|
+
options.leaseDurationMs < MIN_LEASE_DURATION_MS ||
|
|
163
|
+
options.leaseDurationMs > MAX_LEASE_DURATION_MS
|
|
164
|
+
) {
|
|
165
|
+
throw new TypeError("Receiver Core leaseDurationMs must be between one second and five minutes");
|
|
166
|
+
}
|
|
167
|
+
if (
|
|
168
|
+
!Number.isSafeInteger(options.maximumDeliveryAttempts) ||
|
|
169
|
+
options.maximumDeliveryAttempts < 1 ||
|
|
170
|
+
options.maximumDeliveryAttempts > MAX_DELIVERY_ATTEMPTS_LIMIT
|
|
171
|
+
) {
|
|
172
|
+
throw new TypeError("Receiver Core maximumDeliveryAttempts must be between 1 and 100");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
this.#store = options.store;
|
|
176
|
+
this.#keyResolver = options.keyResolver;
|
|
177
|
+
this.#consentAuthority = options.consentAuthority;
|
|
178
|
+
this.#grantControlAuthority = options.grantControlAuthority;
|
|
179
|
+
this.#maximumGrantLifetimeMs = options.maximumGrantLifetimeMs;
|
|
180
|
+
this.#maximumDeliveryAttempts = options.maximumDeliveryAttempts;
|
|
181
|
+
this.#clock = options.clock ?? (() => new Date());
|
|
182
|
+
this.#createId = options.createId ?? ((prefix) => `${prefix}_${randomUUID()}`);
|
|
183
|
+
if (typeof this.#clock !== "function") {
|
|
184
|
+
throw new TypeError("Receiver Core clock must be a function");
|
|
185
|
+
}
|
|
186
|
+
if (typeof this.#createId !== "function") {
|
|
187
|
+
throw new TypeError("Receiver Core createId must be a function");
|
|
188
|
+
}
|
|
189
|
+
this.#delivery = new ReceiverDelivery({
|
|
190
|
+
store: options.store,
|
|
191
|
+
connectorAuthority: options.connectorAuthority,
|
|
192
|
+
effectAuthority: options.effectAuthority,
|
|
193
|
+
leaseDurationMs: options.leaseDurationMs,
|
|
194
|
+
clock: this.#clock,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
createConsentChallenge(input) {
|
|
199
|
+
requireExactInput(
|
|
200
|
+
input,
|
|
201
|
+
CREATE_CHALLENGE_FIELDS,
|
|
202
|
+
CREATE_CHALLENGE_FIELDS,
|
|
203
|
+
"Consent challenge input",
|
|
204
|
+
);
|
|
205
|
+
const now = this.#readClock();
|
|
206
|
+
const manifest = validateReentryManifest(input.manifest, {
|
|
207
|
+
keyResolver: this.#keyResolver,
|
|
208
|
+
expectedOrigin: input.expectedOrigin,
|
|
209
|
+
now,
|
|
210
|
+
});
|
|
211
|
+
const manifestJson = canonicalJson(manifest);
|
|
212
|
+
const maximumExpiresAt = new Date(now.getTime() + this.#maximumGrantLifetimeMs);
|
|
213
|
+
if (!Number.isFinite(maximumExpiresAt.getTime())) {
|
|
214
|
+
throw new TypeError("Receiver Core maximum Grant lifetime exceeds the Date range");
|
|
215
|
+
}
|
|
216
|
+
const effectiveExpiresAt = new Date(Math.min(
|
|
217
|
+
Date.parse(manifest.grant_request.grant_expires_at),
|
|
218
|
+
maximumExpiresAt.getTime(),
|
|
219
|
+
)).toISOString();
|
|
220
|
+
const challenge = {
|
|
221
|
+
challenge_id: this.#nextId("challenge"),
|
|
222
|
+
manifest_id: manifest.manifest_id,
|
|
223
|
+
manifest_json: manifestJson,
|
|
224
|
+
expected_origin: manifest.issuer_origin,
|
|
225
|
+
effective_expires_at: effectiveExpiresAt,
|
|
226
|
+
status: "pending",
|
|
227
|
+
decision_id: null,
|
|
228
|
+
decision_action: null,
|
|
229
|
+
subject_id: null,
|
|
230
|
+
created_at: now.toISOString(),
|
|
231
|
+
decided_at: null,
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
return this.#store.transaction((transaction) => {
|
|
235
|
+
const existing = transaction.getChallengeByManifestId(manifest.manifest_id);
|
|
236
|
+
if (existing) {
|
|
237
|
+
if (existing.manifest_json !== manifestJson) {
|
|
238
|
+
throw conflict(
|
|
239
|
+
"manifest_identity_conflict",
|
|
240
|
+
"Manifest ID is already attached to different canonical content",
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
return deepFreeze({
|
|
244
|
+
challenge: buildPublicChallenge(existing, now),
|
|
245
|
+
duplicate: true,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
transaction.insertChallenge(challenge);
|
|
249
|
+
return deepFreeze({
|
|
250
|
+
challenge: buildPublicChallenge(challenge, now),
|
|
251
|
+
duplicate: false,
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
getConsentChallenge(challengeId) {
|
|
257
|
+
const normalizedId = requireIdentifier(challengeId, "challengeId");
|
|
258
|
+
const challenge = this.#store.getChallengeById(normalizedId);
|
|
259
|
+
if (!challenge) {
|
|
260
|
+
throw notFound("challenge_not_found", "Consent challenge was not found");
|
|
261
|
+
}
|
|
262
|
+
return buildPublicChallenge(challenge, this.#readClock());
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
decideConsent(input) {
|
|
266
|
+
requireExactInput(
|
|
267
|
+
input,
|
|
268
|
+
DECIDE_CONSENT_FIELDS,
|
|
269
|
+
DECIDE_CONSENT_FIELDS,
|
|
270
|
+
"Consent decision input",
|
|
271
|
+
);
|
|
272
|
+
const challengeId = requireIdentifier(input.challengeId, "challengeId");
|
|
273
|
+
const decisionToken = requireDecisionToken(input.decisionToken);
|
|
274
|
+
const now = this.#readClock();
|
|
275
|
+
const initial = this.#store.getChallengeById(challengeId);
|
|
276
|
+
if (!initial) {
|
|
277
|
+
throw notFound("challenge_not_found", "Consent challenge was not found");
|
|
278
|
+
}
|
|
279
|
+
const decision = this.#verifyDecision(challengeId, decisionToken, initial, now);
|
|
280
|
+
|
|
281
|
+
if (initial.status !== "pending") {
|
|
282
|
+
return this.#terminalDecisionResponse(initial, decision, now, true);
|
|
283
|
+
}
|
|
284
|
+
if (decision.action === "decline") {
|
|
285
|
+
return this.#store.transaction((transaction) => {
|
|
286
|
+
const current = requireChallenge(transaction, challengeId);
|
|
287
|
+
if (current.status !== "pending") {
|
|
288
|
+
return this.#terminalDecisionResponse(current, decision, now, true, transaction);
|
|
289
|
+
}
|
|
290
|
+
const changed = transaction.setChallengeDecision({
|
|
291
|
+
challenge_id: challengeId,
|
|
292
|
+
status: "declined",
|
|
293
|
+
decision_id: decision.decision_id,
|
|
294
|
+
decision_action: "decline",
|
|
295
|
+
subject_id: decision.subject_id,
|
|
296
|
+
decided_at: decision.decided_at,
|
|
297
|
+
});
|
|
298
|
+
if (!changed) {
|
|
299
|
+
throw conflict("consent_decision_race", "Consent decision claim was lost");
|
|
300
|
+
}
|
|
301
|
+
return decisionResponse("declined", challengeId, false);
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const manifest = validateReentryManifest(JSON.parse(initial.manifest_json), {
|
|
306
|
+
keyResolver: this.#keyResolver,
|
|
307
|
+
expectedOrigin: initial.expected_origin,
|
|
308
|
+
now,
|
|
309
|
+
});
|
|
310
|
+
assertApprovalWindow(initial, manifest, decision, now);
|
|
311
|
+
const grantId = this.#nextId("grant");
|
|
312
|
+
const bindingId = this.#nextId("binding");
|
|
313
|
+
const receipt = createContinuationReceipt({
|
|
314
|
+
type: "webmcp.continuation_receipt",
|
|
315
|
+
protocol_version: PROTOCOL_VERSION,
|
|
316
|
+
grant_id: grantId,
|
|
317
|
+
correlation_id: manifest.correlation_id,
|
|
318
|
+
issuer_origin: manifest.issuer_origin,
|
|
319
|
+
workflow_id: manifest.workflow.id,
|
|
320
|
+
event_type: manifest.grant_request.event_type,
|
|
321
|
+
canonical_url: manifest.workflow.canonical_url,
|
|
322
|
+
expires_at: initial.effective_expires_at,
|
|
323
|
+
human_boundary: manifest.grant_request.human_boundary,
|
|
324
|
+
continuation_mode: "open_canonical_page_read_current_state",
|
|
325
|
+
});
|
|
326
|
+
const grant = {
|
|
327
|
+
grant_id: grantId,
|
|
328
|
+
challenge_id: challengeId,
|
|
329
|
+
manifest_id: manifest.manifest_id,
|
|
330
|
+
binding_id: bindingId,
|
|
331
|
+
subject_id: decision.subject_id,
|
|
332
|
+
delivery_target_id: decision.delivery_target_id,
|
|
333
|
+
correlation_id: manifest.correlation_id,
|
|
334
|
+
issuer_origin: manifest.issuer_origin,
|
|
335
|
+
workflow_type: manifest.workflow.type,
|
|
336
|
+
workflow_id: manifest.workflow.id,
|
|
337
|
+
event_type: manifest.grant_request.event_type,
|
|
338
|
+
canonical_url: manifest.workflow.canonical_url,
|
|
339
|
+
expires_at: initial.effective_expires_at,
|
|
340
|
+
human_boundary: manifest.grant_request.human_boundary,
|
|
341
|
+
runs_remaining: 1,
|
|
342
|
+
revoked_at: null,
|
|
343
|
+
receipt_json: canonicalJson(receipt),
|
|
344
|
+
created_at: now.toISOString(),
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
return this.#store.transaction((transaction) => {
|
|
348
|
+
const current = requireChallenge(transaction, challengeId);
|
|
349
|
+
if (current.status !== "pending") {
|
|
350
|
+
return this.#terminalDecisionResponse(current, decision, now, true, transaction);
|
|
351
|
+
}
|
|
352
|
+
if (
|
|
353
|
+
current.manifest_json !== initial.manifest_json ||
|
|
354
|
+
current.effective_expires_at !== initial.effective_expires_at
|
|
355
|
+
) {
|
|
356
|
+
throw invariant("challenge_changed", "Consent challenge changed during approval");
|
|
357
|
+
}
|
|
358
|
+
transaction.insertGrant(grant);
|
|
359
|
+
const changed = transaction.setChallengeDecision({
|
|
360
|
+
challenge_id: challengeId,
|
|
361
|
+
status: "approved",
|
|
362
|
+
decision_id: decision.decision_id,
|
|
363
|
+
decision_action: "approve",
|
|
364
|
+
subject_id: decision.subject_id,
|
|
365
|
+
decided_at: decision.decided_at,
|
|
366
|
+
});
|
|
367
|
+
if (!changed) {
|
|
368
|
+
throw conflict("consent_decision_race", "Consent decision claim was lost");
|
|
369
|
+
}
|
|
370
|
+
return approvalResponse(challengeId, grant, now, false);
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
acceptEvent(envelope) {
|
|
375
|
+
requireExactInput(envelope, ENVELOPE_FIELDS, ENVELOPE_FIELDS, "Event envelope");
|
|
376
|
+
const parsedEvent = parseContinuationEventBody(envelope.body);
|
|
377
|
+
const initialGrant = this.#store.getGrantByBindingId(parsedEvent.binding_id);
|
|
378
|
+
if (!initialGrant) {
|
|
379
|
+
throw scope("event_scope_invalid", "Event does not resolve to an eligible Grant");
|
|
380
|
+
}
|
|
381
|
+
const now = this.#readClock();
|
|
382
|
+
const event = verifyContinuationEventEnvelope(envelope, {
|
|
383
|
+
keyResolver: this.#keyResolver,
|
|
384
|
+
expectedOrigin: initialGrant.issuer_origin,
|
|
385
|
+
now,
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
return this.#store.transaction((transaction) => {
|
|
389
|
+
const existing = transaction.getEventById(event.event_id);
|
|
390
|
+
if (existing) {
|
|
391
|
+
if (
|
|
392
|
+
existing.grant_id !== initialGrant.grant_id ||
|
|
393
|
+
existing.canonical_body !== envelope.body
|
|
394
|
+
) {
|
|
395
|
+
throw conflict(
|
|
396
|
+
"event_identity_conflict",
|
|
397
|
+
"Event ID is already attached to different canonical content",
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
const prior = JSON.parse(existing.acceptance_json);
|
|
401
|
+
return createContinuationAcceptance({ ...prior, duplicate: true });
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const grant = transaction.getGrantByBindingId(event.binding_id);
|
|
405
|
+
if (!grant) {
|
|
406
|
+
throw scope("event_scope_invalid", "Event does not resolve to an eligible Grant");
|
|
407
|
+
}
|
|
408
|
+
validateEventAgainstGrant(event, grant, now);
|
|
409
|
+
const deliveryId = this.#nextId("delivery");
|
|
410
|
+
const acceptance = createContinuationAcceptance({
|
|
411
|
+
type: ACCEPTANCE_TYPE,
|
|
412
|
+
protocol_version: PROTOCOL_VERSION,
|
|
413
|
+
event_id: event.event_id,
|
|
414
|
+
correlation_id: event.correlation_id,
|
|
415
|
+
accepted: true,
|
|
416
|
+
duplicate: false,
|
|
417
|
+
status: "accepted",
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
if (!transaction.consumeGrantRun(grant.grant_id)) {
|
|
421
|
+
throw conflict("grant_reservation_lost", "Grant run reservation was lost");
|
|
422
|
+
}
|
|
423
|
+
transaction.insertEvent({
|
|
424
|
+
event_id: event.event_id,
|
|
425
|
+
grant_id: grant.grant_id,
|
|
426
|
+
canonical_body: envelope.body,
|
|
427
|
+
acceptance_json: canonicalJson(acceptance),
|
|
428
|
+
received_at: now.toISOString(),
|
|
429
|
+
});
|
|
430
|
+
transaction.insertDelivery({
|
|
431
|
+
delivery_id: deliveryId,
|
|
432
|
+
event_id: event.event_id,
|
|
433
|
+
grant_id: grant.grant_id,
|
|
434
|
+
delivery_target_id: grant.delivery_target_id,
|
|
435
|
+
status: "pending",
|
|
436
|
+
maximum_attempts: this.#maximumDeliveryAttempts,
|
|
437
|
+
created_at: now.toISOString(),
|
|
438
|
+
});
|
|
439
|
+
return acceptance;
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
inspectGrant(input) {
|
|
444
|
+
requireExactInput(
|
|
445
|
+
input,
|
|
446
|
+
GRANT_CONTROL_INPUT_FIELDS,
|
|
447
|
+
GRANT_CONTROL_INPUT_FIELDS,
|
|
448
|
+
"Grant inspection input",
|
|
449
|
+
);
|
|
450
|
+
const bindingId = requireIdentifier(input.bindingId, "bindingId");
|
|
451
|
+
const controlToken = requireGrantControlToken(input.controlToken);
|
|
452
|
+
const now = this.#readClock();
|
|
453
|
+
const authorization = this.#verifyGrantControl(
|
|
454
|
+
bindingId,
|
|
455
|
+
"inspect",
|
|
456
|
+
controlToken,
|
|
457
|
+
now,
|
|
458
|
+
);
|
|
459
|
+
const grant = this.#store.getGrantByBindingId(bindingId);
|
|
460
|
+
if (!grant) {
|
|
461
|
+
throw notFound("grant_not_found", "Grant was not found");
|
|
462
|
+
}
|
|
463
|
+
assertGrantControlSubject(grant, authorization);
|
|
464
|
+
return grantSummary(grant, now);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
revokeGrant(input) {
|
|
468
|
+
requireExactInput(
|
|
469
|
+
input,
|
|
470
|
+
GRANT_CONTROL_INPUT_FIELDS,
|
|
471
|
+
GRANT_CONTROL_INPUT_FIELDS,
|
|
472
|
+
"Grant revocation input",
|
|
473
|
+
);
|
|
474
|
+
const bindingId = requireIdentifier(input.bindingId, "bindingId");
|
|
475
|
+
const controlToken = requireGrantControlToken(input.controlToken);
|
|
476
|
+
const now = this.#readClock();
|
|
477
|
+
const authorization = this.#verifyGrantControl(
|
|
478
|
+
bindingId,
|
|
479
|
+
"revoke",
|
|
480
|
+
controlToken,
|
|
481
|
+
now,
|
|
482
|
+
);
|
|
483
|
+
const revokedAt = now.toISOString();
|
|
484
|
+
|
|
485
|
+
return this.#store.transaction((transaction) => {
|
|
486
|
+
const grant = transaction.getGrantByBindingId(bindingId);
|
|
487
|
+
if (!grant) {
|
|
488
|
+
throw notFound("grant_not_found", "Grant was not found");
|
|
489
|
+
}
|
|
490
|
+
assertGrantControlSubject(grant, authorization);
|
|
491
|
+
if (grant.revoked_at !== null) {
|
|
492
|
+
return grantRevocation(grant, true);
|
|
493
|
+
}
|
|
494
|
+
if (!transaction.revokeGrant(grant.grant_id, revokedAt)) {
|
|
495
|
+
const current = transaction.getGrantByBindingId(bindingId);
|
|
496
|
+
if (current && current.revoked_at !== null) {
|
|
497
|
+
assertGrantControlSubject(current, authorization);
|
|
498
|
+
return grantRevocation(current, true);
|
|
499
|
+
}
|
|
500
|
+
throw conflict("grant_revocation_race", "Grant revocation claim was lost");
|
|
501
|
+
}
|
|
502
|
+
return grantRevocation({ ...grant, revoked_at: revokedAt }, false);
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
claimDelivery(input) {
|
|
507
|
+
return this.#delivery.claimDelivery(input);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
acknowledgeDelivery(input) {
|
|
511
|
+
return this.#delivery.acknowledgeDelivery(input);
|
|
512
|
+
}
|
|
513
|
+
#verifyDecision(challengeId, token, challenge, now) {
|
|
514
|
+
let value;
|
|
515
|
+
try {
|
|
516
|
+
value = this.#consentAuthority.verifyDecision({
|
|
517
|
+
challengeId,
|
|
518
|
+
decisionToken: token,
|
|
519
|
+
});
|
|
520
|
+
} catch {
|
|
521
|
+
throw authorization(
|
|
522
|
+
"consent_decision_invalid",
|
|
523
|
+
"Consent decision could not be verified by the Receiver authority",
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
if (value === undefined || value === null) {
|
|
527
|
+
throw authorization(
|
|
528
|
+
"consent_decision_invalid",
|
|
529
|
+
"Consent decision could not be verified by the Receiver authority",
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
return normalizeConsentDecision(value, challenge, now);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
#verifyGrantControl(bindingId, action, token, now) {
|
|
536
|
+
let value;
|
|
537
|
+
try {
|
|
538
|
+
value = this.#grantControlAuthority.verifyControl({
|
|
539
|
+
bindingId,
|
|
540
|
+
action,
|
|
541
|
+
controlToken: token,
|
|
542
|
+
});
|
|
543
|
+
} catch {
|
|
544
|
+
throw authorization(
|
|
545
|
+
"grant_control_invalid",
|
|
546
|
+
"Grant control could not be verified by the Receiver authority",
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
if (value === undefined || value === null) {
|
|
550
|
+
throw authorization(
|
|
551
|
+
"grant_control_invalid",
|
|
552
|
+
"Grant control could not be verified by the Receiver authority",
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
return normalizeGrantControlAuthorization(value, bindingId, action, now);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
#terminalDecisionResponse(challenge, decision, now, duplicate, transaction = this.#store) {
|
|
559
|
+
if (
|
|
560
|
+
challenge.decision_id !== decision.decision_id ||
|
|
561
|
+
challenge.decision_action !== decision.action
|
|
562
|
+
) {
|
|
563
|
+
throw conflict("consent_already_decided", "Consent challenge has a different terminal decision");
|
|
564
|
+
}
|
|
565
|
+
if (
|
|
566
|
+
challenge.subject_id !== decision.subject_id ||
|
|
567
|
+
challenge.decided_at !== decision.decided_at
|
|
568
|
+
) {
|
|
569
|
+
throw conflict(
|
|
570
|
+
"consent_decision_identity_conflict",
|
|
571
|
+
"Consent decision ID is attached to different attestation content",
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
if (challenge.status === "declined") {
|
|
575
|
+
return decisionResponse("declined", challenge.challenge_id, duplicate);
|
|
576
|
+
}
|
|
577
|
+
if (challenge.status !== "approved") {
|
|
578
|
+
throw invariant("challenge_status_invalid", "Consent challenge has an invalid terminal state");
|
|
579
|
+
}
|
|
580
|
+
const grant = transaction.getGrantByChallengeId(challenge.challenge_id);
|
|
581
|
+
if (!grant) {
|
|
582
|
+
throw invariant("approved_grant_missing", "Approved consent challenge has no Grant");
|
|
583
|
+
}
|
|
584
|
+
if (grant.delivery_target_id !== decision.delivery_target_id) {
|
|
585
|
+
throw conflict(
|
|
586
|
+
"consent_decision_identity_conflict",
|
|
587
|
+
"Consent decision ID is attached to different attestation content",
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
return approvalResponse(challenge.challenge_id, grant, now, duplicate);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
#readClock() {
|
|
594
|
+
const value = this.#clock();
|
|
595
|
+
if (!(value instanceof Date) || !Number.isFinite(value.getTime())) {
|
|
596
|
+
throw new TypeError("Receiver Core clock must return a valid Date");
|
|
597
|
+
}
|
|
598
|
+
return new Date(value.getTime());
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
#nextId(prefix) {
|
|
602
|
+
const value = this.#createId(prefix);
|
|
603
|
+
if (typeof value !== "string") {
|
|
604
|
+
throw new TypeError("Receiver Core createId must return a string");
|
|
605
|
+
}
|
|
606
|
+
return requireIdentifier(value, `${prefix} ID`);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function buildPublicChallenge(challenge, now) {
|
|
611
|
+
const manifest = JSON.parse(challenge.manifest_json);
|
|
612
|
+
const expired = challenge.status === "pending" && (
|
|
613
|
+
Date.parse(manifest.offer_expires_at) <= now.getTime() ||
|
|
614
|
+
Date.parse(challenge.effective_expires_at) <= now.getTime()
|
|
615
|
+
);
|
|
616
|
+
return deepFreeze({
|
|
617
|
+
challenge_id: challenge.challenge_id,
|
|
618
|
+
manifest_id: challenge.manifest_id,
|
|
619
|
+
correlation_id: manifest.correlation_id,
|
|
620
|
+
status: expired ? "expired" : challenge.status,
|
|
621
|
+
issuer_origin: manifest.issuer_origin,
|
|
622
|
+
offer_expires_at: manifest.offer_expires_at,
|
|
623
|
+
workflow: {
|
|
624
|
+
id: manifest.workflow.id,
|
|
625
|
+
type: manifest.workflow.type,
|
|
626
|
+
canonical_url: manifest.workflow.canonical_url,
|
|
627
|
+
},
|
|
628
|
+
display: {
|
|
629
|
+
title: manifest.display.title,
|
|
630
|
+
reason: manifest.display.reason,
|
|
631
|
+
},
|
|
632
|
+
grant_scope: {
|
|
633
|
+
event_type: manifest.grant_request.event_type,
|
|
634
|
+
expires_at: challenge.effective_expires_at,
|
|
635
|
+
max_runs: 1,
|
|
636
|
+
human_boundary: manifest.grant_request.human_boundary,
|
|
637
|
+
},
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function normalizeConsentDecision(value, challenge, now) {
|
|
642
|
+
requireExactInput(
|
|
643
|
+
value,
|
|
644
|
+
APPROVAL_DECISION_FIELDS,
|
|
645
|
+
DECLINE_DECISION_FIELDS,
|
|
646
|
+
"Consent decision attestation",
|
|
647
|
+
);
|
|
648
|
+
if (!["approve", "decline"].includes(value.action)) {
|
|
649
|
+
throw authorization("consent_decision_action_invalid", "Consent decision action is unsupported");
|
|
650
|
+
}
|
|
651
|
+
requireExactInput(
|
|
652
|
+
value,
|
|
653
|
+
value.action === "approve" ? APPROVAL_DECISION_FIELDS : DECLINE_DECISION_FIELDS,
|
|
654
|
+
value.action === "approve" ? APPROVAL_DECISION_FIELDS : DECLINE_DECISION_FIELDS,
|
|
655
|
+
"Consent decision attestation",
|
|
656
|
+
);
|
|
657
|
+
if (value.type !== CONSENT_DECISION_TYPE || value.protocol_version !== PROTOCOL_VERSION) {
|
|
658
|
+
throw authorization("consent_decision_version_invalid", "Consent decision version is unsupported");
|
|
659
|
+
}
|
|
660
|
+
const normalized = {
|
|
661
|
+
type: CONSENT_DECISION_TYPE,
|
|
662
|
+
protocol_version: PROTOCOL_VERSION,
|
|
663
|
+
decision_id: requireIdentifier(value.decision_id, "decision_id"),
|
|
664
|
+
challenge_id: requireIdentifier(value.challenge_id, "decision challenge_id"),
|
|
665
|
+
action: value.action,
|
|
666
|
+
subject_id: requireIdentifier(value.subject_id, "decision subject_id"),
|
|
667
|
+
...(value.action === "approve"
|
|
668
|
+
? { delivery_target_id: requireIdentifier(value.delivery_target_id, "decision delivery_target_id") }
|
|
669
|
+
: {}),
|
|
670
|
+
decided_at: requireTimestamp(value.decided_at, "decision decided_at"),
|
|
671
|
+
};
|
|
672
|
+
if (normalized.challenge_id !== challenge.challenge_id) {
|
|
673
|
+
throw authorization("consent_decision_scope_invalid", "Consent decision is for another challenge");
|
|
674
|
+
}
|
|
675
|
+
const decidedAt = Date.parse(normalized.decided_at);
|
|
676
|
+
if (
|
|
677
|
+
decidedAt < Date.parse(challenge.created_at) ||
|
|
678
|
+
decidedAt > now.getTime() + AUTHORITY_FUTURE_SKEW_MS
|
|
679
|
+
) {
|
|
680
|
+
throw authorization("consent_decision_time_invalid", "Consent decision time is outside its valid window");
|
|
681
|
+
}
|
|
682
|
+
return deepFreeze(normalized);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
function normalizeGrantControlAuthorization(value, bindingId, action, now) {
|
|
686
|
+
requireExactInput(
|
|
687
|
+
value,
|
|
688
|
+
GRANT_CONTROL_AUTHORIZATION_FIELDS,
|
|
689
|
+
GRANT_CONTROL_AUTHORIZATION_FIELDS,
|
|
690
|
+
"Grant control authorization",
|
|
691
|
+
);
|
|
692
|
+
if (
|
|
693
|
+
value.type !== GRANT_CONTROL_AUTHORIZATION_TYPE ||
|
|
694
|
+
value.protocol_version !== PROTOCOL_VERSION
|
|
695
|
+
) {
|
|
696
|
+
throw authorization(
|
|
697
|
+
"grant_control_version_invalid",
|
|
698
|
+
"Grant control version is unsupported",
|
|
699
|
+
);
|
|
700
|
+
}
|
|
701
|
+
const normalized = {
|
|
702
|
+
type: GRANT_CONTROL_AUTHORIZATION_TYPE,
|
|
703
|
+
protocol_version: PROTOCOL_VERSION,
|
|
704
|
+
binding_id: requireIdentifier(value.binding_id, "control binding_id"),
|
|
705
|
+
action: value.action,
|
|
706
|
+
subject_id: requireIdentifier(value.subject_id, "control subject_id"),
|
|
707
|
+
authenticated_at: requireTimestamp(value.authenticated_at, "control authenticated_at"),
|
|
708
|
+
expires_at: requireTimestamp(value.expires_at, "control expires_at"),
|
|
709
|
+
};
|
|
710
|
+
if (!["inspect", "revoke"].includes(normalized.action)) {
|
|
711
|
+
throw authorization("grant_control_action_invalid", "Grant control action is unsupported");
|
|
712
|
+
}
|
|
713
|
+
if (normalized.binding_id !== bindingId || normalized.action !== action) {
|
|
714
|
+
throw authorization("grant_control_scope_invalid", "Grant control is for another operation");
|
|
715
|
+
}
|
|
716
|
+
const authenticatedAt = Date.parse(normalized.authenticated_at);
|
|
717
|
+
const expiresAt = Date.parse(normalized.expires_at);
|
|
718
|
+
if (
|
|
719
|
+
authenticatedAt > now.getTime() + AUTHORITY_FUTURE_SKEW_MS ||
|
|
720
|
+
authenticatedAt >= expiresAt ||
|
|
721
|
+
expiresAt <= now.getTime()
|
|
722
|
+
) {
|
|
723
|
+
throw authorization("grant_control_time_invalid", "Grant control is outside its valid window");
|
|
724
|
+
}
|
|
725
|
+
return deepFreeze(normalized);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
function assertApprovalWindow(challenge, manifest, decision, now) {
|
|
729
|
+
const effectiveExpiry = Date.parse(challenge.effective_expires_at);
|
|
730
|
+
if (effectiveExpiry <= now.getTime()) {
|
|
731
|
+
throw conflict("grant_window_expired", "Effective Grant window has expired", 410);
|
|
732
|
+
}
|
|
733
|
+
if (
|
|
734
|
+
Date.parse(decision.decided_at) >= effectiveExpiry ||
|
|
735
|
+
Date.parse(decision.decided_at) >= Date.parse(manifest.offer_expires_at)
|
|
736
|
+
) {
|
|
737
|
+
throw authorization("consent_decision_time_invalid", "Approval occurred outside the consent window");
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
function validateEventAgainstGrant(event, grant, now) {
|
|
742
|
+
if (
|
|
743
|
+
event.binding_id !== grant.binding_id ||
|
|
744
|
+
event.correlation_id !== grant.correlation_id ||
|
|
745
|
+
event.issuer_origin !== grant.issuer_origin ||
|
|
746
|
+
event.workflow_id !== grant.workflow_id ||
|
|
747
|
+
event.event_type !== grant.event_type ||
|
|
748
|
+
event.canonical_url !== grant.canonical_url
|
|
749
|
+
) {
|
|
750
|
+
throw scope("event_scope_invalid", "Event is outside the approved Grant scope");
|
|
751
|
+
}
|
|
752
|
+
if (grant.revoked_at !== null) {
|
|
753
|
+
throw scope("grant_revoked", "Grant is revoked");
|
|
754
|
+
}
|
|
755
|
+
if (Date.parse(grant.expires_at) <= now.getTime()) {
|
|
756
|
+
throw scope("grant_expired", "Grant is expired", 410);
|
|
757
|
+
}
|
|
758
|
+
if (grant.runs_remaining !== 1) {
|
|
759
|
+
throw conflict("grant_exhausted", "Grant run budget is exhausted");
|
|
760
|
+
}
|
|
761
|
+
if (Date.parse(event.occurred_at) >= Date.parse(grant.expires_at)) {
|
|
762
|
+
throw scope("event_after_grant_expiry", "Event occurred outside the Grant window");
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
function approvalResponse(challengeId, grant, now, duplicate) {
|
|
767
|
+
return deepFreeze({
|
|
768
|
+
challenge_id: challengeId,
|
|
769
|
+
status: "approved",
|
|
770
|
+
duplicate,
|
|
771
|
+
binding: publicBindingFromGrant(grant, now),
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
function decisionResponse(status, challengeId, duplicate) {
|
|
776
|
+
return deepFreeze({
|
|
777
|
+
challenge_id: challengeId,
|
|
778
|
+
status,
|
|
779
|
+
duplicate,
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
function publicBindingFromGrant(grant, now) {
|
|
784
|
+
return validatePublicBinding({
|
|
785
|
+
type: PUBLIC_BINDING_TYPE,
|
|
786
|
+
protocol_version: PROTOCOL_VERSION,
|
|
787
|
+
binding_id: grant.binding_id,
|
|
788
|
+
correlation_id: grant.correlation_id,
|
|
789
|
+
workflow_id: grant.workflow_id,
|
|
790
|
+
event_type: grant.event_type,
|
|
791
|
+
expires_at: grant.expires_at,
|
|
792
|
+
runs_remaining: grant.runs_remaining,
|
|
793
|
+
status: grantStatus(grant, now),
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
function grantSummary(grant, now) {
|
|
798
|
+
return deepFreeze({
|
|
799
|
+
type: GRANT_SUMMARY_TYPE,
|
|
800
|
+
protocol_version: PROTOCOL_VERSION,
|
|
801
|
+
binding_id: grant.binding_id,
|
|
802
|
+
correlation_id: grant.correlation_id,
|
|
803
|
+
issuer_origin: grant.issuer_origin,
|
|
804
|
+
workflow_type: grant.workflow_type,
|
|
805
|
+
workflow_id: grant.workflow_id,
|
|
806
|
+
event_type: grant.event_type,
|
|
807
|
+
canonical_url: grant.canonical_url,
|
|
808
|
+
expires_at: grant.expires_at,
|
|
809
|
+
human_boundary: grant.human_boundary,
|
|
810
|
+
runs_remaining: grant.runs_remaining,
|
|
811
|
+
status: grantStatus(grant, now),
|
|
812
|
+
created_at: grant.created_at,
|
|
813
|
+
revoked_at: grant.revoked_at,
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
function grantRevocation(grant, duplicate) {
|
|
818
|
+
return deepFreeze({
|
|
819
|
+
type: GRANT_REVOCATION_TYPE,
|
|
820
|
+
protocol_version: PROTOCOL_VERSION,
|
|
821
|
+
binding_id: grant.binding_id,
|
|
822
|
+
status: "revoked",
|
|
823
|
+
revoked_at: grant.revoked_at,
|
|
824
|
+
duplicate,
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
function grantStatus(grant, now) {
|
|
829
|
+
if (grant.revoked_at !== null) return "revoked";
|
|
830
|
+
if (Date.parse(grant.expires_at) <= now.getTime()) return "expired";
|
|
831
|
+
if (grant.runs_remaining === 0) return "exhausted";
|
|
832
|
+
return "active";
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
function assertGrantControlSubject(grant, control) {
|
|
836
|
+
if (grant.subject_id !== control.subject_id) {
|
|
837
|
+
throw authorization(
|
|
838
|
+
"grant_control_scope_invalid",
|
|
839
|
+
"Grant control subject does not own this Grant",
|
|
840
|
+
);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
function requireChallenge(store, challengeId) {
|
|
845
|
+
const challenge = store.getChallengeById(challengeId);
|
|
846
|
+
if (!challenge) throw notFound("challenge_not_found", "Consent challenge was not found");
|
|
847
|
+
return challenge;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
function requireStore(store) {
|
|
851
|
+
if (!store || typeof store !== "object") {
|
|
852
|
+
throw new TypeError("Receiver Core store must implement the persistence port");
|
|
853
|
+
}
|
|
854
|
+
for (const method of STORE_METHODS) {
|
|
855
|
+
if (typeof store[method] !== "function") {
|
|
856
|
+
throw new TypeError(`Receiver Core store is missing ${method}`);
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
function requireDecisionToken(value) {
|
|
862
|
+
return requireOpaqueToken(value, "Consent decision token", "consent_token_invalid");
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
function requireGrantControlToken(value) {
|
|
866
|
+
return requireOpaqueToken(value, "Grant control token", "grant_control_token_invalid");
|
|
867
|
+
}
|