@geostack/arc 0.1.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/LICENSE +21 -0
- package/README.md +122 -0
- package/dist/cli.d.ts +8 -0
- package/dist/cli.js +890 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +632 -0
- package/dist/index.js +1514 -0
- package/dist/index.js.map +1 -0
- package/package.json +42 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1514 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { decodeProtectedHeader, importJWK, compactVerify } from "jose";
|
|
3
|
+
const defaultMaxTimestampSkewMs = 5 * 60 * 1000;
|
|
4
|
+
const defaultNonceStore = createMemoryNonceStore();
|
|
5
|
+
const actionKeyPattern = /^[a-z][a-z0-9_.:-]{0,79}$/;
|
|
6
|
+
const slugPattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
7
|
+
const defaultBaseUrl = "http://127.0.0.1:4000";
|
|
8
|
+
const riskLevels = new Set(["low", "medium", "high", "critical"]);
|
|
9
|
+
const defaultDecisions = new Set(["allow", "ask", "block"]);
|
|
10
|
+
export class ArcError extends Error {
|
|
11
|
+
code;
|
|
12
|
+
constructor(code, message) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.code = code;
|
|
15
|
+
this.name = "ArcError";
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export class ArcValidationError extends ArcError {
|
|
19
|
+
constructor(code, message) {
|
|
20
|
+
super(code, message);
|
|
21
|
+
this.name = "ArcValidationError";
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export class ArcActionError extends ArcError {
|
|
25
|
+
statusCode;
|
|
26
|
+
constructor(statusCode, code, message) {
|
|
27
|
+
super(code, message);
|
|
28
|
+
this.statusCode = statusCode;
|
|
29
|
+
this.name = "ArcActionError";
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export class ArcHttpError extends ArcError {
|
|
33
|
+
status;
|
|
34
|
+
responseBody;
|
|
35
|
+
constructor(status, code, message, responseBody) {
|
|
36
|
+
super(code, message);
|
|
37
|
+
this.status = status;
|
|
38
|
+
this.responseBody = responseBody;
|
|
39
|
+
this.name = "ArcHttpError";
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export class ArcRequestVerificationError extends Error {
|
|
43
|
+
code;
|
|
44
|
+
constructor(code, message) {
|
|
45
|
+
super(message);
|
|
46
|
+
this.code = code;
|
|
47
|
+
this.name = "ArcRequestVerificationError";
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export function defineActions(actions) {
|
|
51
|
+
validateActionDefinitions(actions);
|
|
52
|
+
return actions;
|
|
53
|
+
}
|
|
54
|
+
export function createActionSyncPayload(actions) {
|
|
55
|
+
validateActionDefinitions(actions);
|
|
56
|
+
return {
|
|
57
|
+
actions: Object.entries(actions)
|
|
58
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
59
|
+
.map(([key, action]) => ({
|
|
60
|
+
cost_currency: (action.cost?.currency ?? "USD").toUpperCase(),
|
|
61
|
+
cost_field: action.cost?.mode === "field" ? action.cost.field : null,
|
|
62
|
+
cost_fixed_minor: action.cost?.mode === "fixed" ? action.cost.fixedMinor : null,
|
|
63
|
+
cost_mode: action.cost?.mode ?? "none",
|
|
64
|
+
default_decision: action.defaultDecision,
|
|
65
|
+
description: action.description ?? null,
|
|
66
|
+
enabled: action.enabled ?? true,
|
|
67
|
+
input_schema: action.input,
|
|
68
|
+
key,
|
|
69
|
+
name: action.name,
|
|
70
|
+
output_schema: action.output ?? null,
|
|
71
|
+
risk_level: action.risk,
|
|
72
|
+
tags: action.tags ?? []
|
|
73
|
+
}))
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
export async function syncActions(actions, options = {}) {
|
|
77
|
+
const payload = createActionSyncPayload(actions);
|
|
78
|
+
if (options.dryRun) {
|
|
79
|
+
return payload;
|
|
80
|
+
}
|
|
81
|
+
if (!options.appId) {
|
|
82
|
+
throw new ArcValidationError("missing_app_id", "syncActions requires appId unless dryRun is true.");
|
|
83
|
+
}
|
|
84
|
+
const client = options.client ??
|
|
85
|
+
new ArcDeveloperClient({
|
|
86
|
+
baseUrl: options.baseUrl,
|
|
87
|
+
sessionCookie: options.sessionCookie
|
|
88
|
+
});
|
|
89
|
+
return client.syncActions(options.appId, payload.actions);
|
|
90
|
+
}
|
|
91
|
+
export function handleAction(actions, handlers, options = {}) {
|
|
92
|
+
validateActionDefinitions(actions);
|
|
93
|
+
const executor = createArcExecutor(options);
|
|
94
|
+
return executor.handleAction(actions, handlers);
|
|
95
|
+
}
|
|
96
|
+
export function createArcExecutor(options = {}) {
|
|
97
|
+
let jwksCache = options.jwks ?? null;
|
|
98
|
+
async function resolveVerifyOptions(localOptions = {}) {
|
|
99
|
+
const jwks = localOptions.jwks ?? jwksCache ?? (await fetchJwks(localOptions));
|
|
100
|
+
jwksCache = jwks;
|
|
101
|
+
return {
|
|
102
|
+
jwks,
|
|
103
|
+
maxTimestampSkewMs: localOptions.maxTimestampSkewMs ?? options.maxTimestampSkewMs,
|
|
104
|
+
nonceStore: localOptions.nonceStore ?? options.nonceStore,
|
|
105
|
+
now: localOptions.now ?? options.now,
|
|
106
|
+
unsafeAllowInMemoryNonceStore: localOptions.unsafeAllowInMemoryNonceStore ?? options.unsafeAllowInMemoryNonceStore
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
async function fetchJwks(localOptions) {
|
|
110
|
+
const jwksUrl = localOptions.jwksUrl ??
|
|
111
|
+
options.jwksUrl ??
|
|
112
|
+
`${trimTrailingSlash(localOptions.apiUrl ?? options.apiUrl ?? defaultBaseUrl)}/.well-known/jwks.json`;
|
|
113
|
+
const response = await fetch(jwksUrl);
|
|
114
|
+
if (!response.ok) {
|
|
115
|
+
throw new ArcRequestVerificationError("jwks_unavailable", "Arc JWKS could not be loaded.");
|
|
116
|
+
}
|
|
117
|
+
const value = await response.json();
|
|
118
|
+
if (!isJwks(value)) {
|
|
119
|
+
throw new ArcRequestVerificationError("jwks_invalid", "Arc JWKS response is invalid.");
|
|
120
|
+
}
|
|
121
|
+
return value;
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
handleAction: (actions, handlers, localOptions = {}) => {
|
|
125
|
+
validateActionDefinitions(actions);
|
|
126
|
+
return async (request, response) => {
|
|
127
|
+
let verified;
|
|
128
|
+
try {
|
|
129
|
+
const normalized = await normalizeActionRequest(request);
|
|
130
|
+
verified = await verifyArcRequest({
|
|
131
|
+
body: normalized.body,
|
|
132
|
+
headers: normalized.headers
|
|
133
|
+
}, await resolveVerifyOptions(localOptions));
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
if (error instanceof ArcRequestVerificationError) {
|
|
137
|
+
return sendActionResponse(request, response, 401, {
|
|
138
|
+
error: {
|
|
139
|
+
code: error.code,
|
|
140
|
+
message: error.message
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
if (error instanceof ArcValidationError) {
|
|
145
|
+
return sendActionResponse(request, response, 400, {
|
|
146
|
+
error: {
|
|
147
|
+
code: error.code,
|
|
148
|
+
message: error.message
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
154
|
+
const actionKey = verified.payload.action_key;
|
|
155
|
+
const isKnownAction = Object.prototype.hasOwnProperty.call(actions, actionKey);
|
|
156
|
+
const handler = isKnownAction ? handlers[actionKey] : undefined;
|
|
157
|
+
if (!isKnownAction || !handler) {
|
|
158
|
+
return sendActionResponse(request, response, 404, {
|
|
159
|
+
error: {
|
|
160
|
+
code: "unknown_action",
|
|
161
|
+
message: "Arc action handler is not registered."
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
try {
|
|
166
|
+
const result = await handler({
|
|
167
|
+
actionKey,
|
|
168
|
+
agent: verified.payload.agent,
|
|
169
|
+
appUserId: verified.payload.app_user_id,
|
|
170
|
+
input: verified.payload.input,
|
|
171
|
+
invocationId: verified.payload.invocation_id,
|
|
172
|
+
payload: verified.payload,
|
|
173
|
+
user: verified.payload.user
|
|
174
|
+
});
|
|
175
|
+
return sendActionResponse(request, response, 200, {
|
|
176
|
+
data: {
|
|
177
|
+
invocation_id: verified.payload.invocation_id,
|
|
178
|
+
result: result ?? null
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
if (error instanceof ArcActionError) {
|
|
184
|
+
return sendActionResponse(request, response, error.statusCode, {
|
|
185
|
+
error: {
|
|
186
|
+
code: error.code,
|
|
187
|
+
message: error.message
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
return sendActionResponse(request, response, 500, {
|
|
192
|
+
error: {
|
|
193
|
+
code: "handler_failed",
|
|
194
|
+
message: "Arc action handler failed."
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
},
|
|
200
|
+
verify: async (request, localOptions = {}) => {
|
|
201
|
+
const normalized = await normalizeActionRequest(request);
|
|
202
|
+
return verifyArcRequest({
|
|
203
|
+
body: normalized.body,
|
|
204
|
+
headers: normalized.headers
|
|
205
|
+
}, await resolveVerifyOptions(localOptions));
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
export async function verifyArcRequest(request, options) {
|
|
210
|
+
const signature = readSignature(request.headers);
|
|
211
|
+
const protectedHeader = parseProtectedHeader(signature);
|
|
212
|
+
if (protectedHeader.alg !== "ES256" ||
|
|
213
|
+
typeof protectedHeader.kid !== "string" ||
|
|
214
|
+
protectedHeader.kid.trim().length === 0) {
|
|
215
|
+
throw new ArcRequestVerificationError("invalid_signature", "Arc signature header is invalid.");
|
|
216
|
+
}
|
|
217
|
+
const key = await importVerificationKey(options.jwks, protectedHeader.kid);
|
|
218
|
+
let verification;
|
|
219
|
+
try {
|
|
220
|
+
verification = await compactVerify(signature, key);
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
throw new ArcRequestVerificationError("invalid_signature", "Arc signature verification failed.");
|
|
224
|
+
}
|
|
225
|
+
const claims = parseClaims(new TextDecoder().decode(verification.payload));
|
|
226
|
+
const bodyJson = bodyToJson(request.body);
|
|
227
|
+
const body = parseBody(bodyJson);
|
|
228
|
+
const bodyHash = `sha256:${sha256Hex(bodyJson)}`;
|
|
229
|
+
if (bodyHash !== claims.body_hash) {
|
|
230
|
+
throw new ArcRequestVerificationError("body_hash_mismatch", "Arc request body hash mismatch.");
|
|
231
|
+
}
|
|
232
|
+
if (body.app_id !== claims.app_id || body.action_key !== claims.action_key) {
|
|
233
|
+
throw new ArcRequestVerificationError("body_claim_mismatch", "Arc request body does not match signature claims.");
|
|
234
|
+
}
|
|
235
|
+
if (body.invocation_id !== claims.invocation_id || body.nonce !== claims.nonce) {
|
|
236
|
+
throw new ArcRequestVerificationError("body_claim_mismatch", "Arc request body does not match signature claims.");
|
|
237
|
+
}
|
|
238
|
+
if (body.timestamp !== claims.timestamp || body.input_hash !== claims.input_hash) {
|
|
239
|
+
throw new ArcRequestVerificationError("body_claim_mismatch", "Arc request body does not match signature claims.");
|
|
240
|
+
}
|
|
241
|
+
if (body.org_id !== claims.org_id || body.workspace_id !== claims.workspace_id) {
|
|
242
|
+
throw new ArcRequestVerificationError("body_claim_mismatch", "Arc request body does not match signature claims.");
|
|
243
|
+
}
|
|
244
|
+
verifyTimestamp(claims.timestamp, options);
|
|
245
|
+
await verifyNonce(claims.nonce, claims.timestamp, options);
|
|
246
|
+
return {
|
|
247
|
+
claims,
|
|
248
|
+
payload: body,
|
|
249
|
+
valid: true
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
export async function verifyArcSignature(request, options) {
|
|
253
|
+
return verifyArcRequest(request, options);
|
|
254
|
+
}
|
|
255
|
+
export async function verifyArcExecution(request, options) {
|
|
256
|
+
const verified = await verifyArcRequest(request, options);
|
|
257
|
+
verifyArcDelegation(verified.payload, options);
|
|
258
|
+
await validateArcInvocation(verified.payload, options);
|
|
259
|
+
return verified;
|
|
260
|
+
}
|
|
261
|
+
export async function verifyArcAuthorityToken(token, options) {
|
|
262
|
+
const protectedHeader = parseProtectedHeader(token);
|
|
263
|
+
if (protectedHeader.alg !== "ES256" ||
|
|
264
|
+
typeof protectedHeader.kid !== "string" ||
|
|
265
|
+
protectedHeader.kid.trim().length === 0) {
|
|
266
|
+
throw new ArcRequestVerificationError("invalid_signature", "Arc authority token header is invalid.");
|
|
267
|
+
}
|
|
268
|
+
const key = await importVerificationKey(options.jwks, protectedHeader.kid);
|
|
269
|
+
let verification;
|
|
270
|
+
try {
|
|
271
|
+
verification = await compactVerify(token, key);
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
throw new ArcRequestVerificationError("invalid_signature", "Arc authority token signature failed.");
|
|
275
|
+
}
|
|
276
|
+
const claims = parseAuthorityClaims(new TextDecoder().decode(verification.payload));
|
|
277
|
+
const nowMs = options.now?.getTime() ?? Date.now();
|
|
278
|
+
const skewMs = options.maxClockSkewMs ?? 30_000;
|
|
279
|
+
if (claims.exp * 1000 <= nowMs - skewMs) {
|
|
280
|
+
throw new ArcRequestVerificationError("token_expired", "Arc authority token is expired.");
|
|
281
|
+
}
|
|
282
|
+
if (claims.iat * 1000 > nowMs + skewMs) {
|
|
283
|
+
throw new ArcRequestVerificationError("token_not_yet_valid", "Arc authority token is not valid yet.");
|
|
284
|
+
}
|
|
285
|
+
if (options.expectedIssuer && claims.iss !== options.expectedIssuer) {
|
|
286
|
+
throw new ArcRequestVerificationError("unexpected_issuer", "Arc authority token issuer does not match.");
|
|
287
|
+
}
|
|
288
|
+
const expectedAudience = options.expectedAudience ?? options.expectedAppId;
|
|
289
|
+
if (expectedAudience && claims.aud !== expectedAudience) {
|
|
290
|
+
throw new ArcRequestVerificationError("unexpected_audience", "Arc authority token audience does not match.");
|
|
291
|
+
}
|
|
292
|
+
if (options.expectedAppId && claims.app_id !== options.expectedAppId) {
|
|
293
|
+
throw new ArcRequestVerificationError("unexpected_app", "Arc authority token app does not match.");
|
|
294
|
+
}
|
|
295
|
+
if (options.expectedActionKey && claims.action_key !== options.expectedActionKey) {
|
|
296
|
+
throw new ArcRequestVerificationError("unexpected_action", "Arc authority token action does not match.");
|
|
297
|
+
}
|
|
298
|
+
if (options.expectedDelegationId && claims.delegation_id !== options.expectedDelegationId) {
|
|
299
|
+
throw new ArcRequestVerificationError("unexpected_delegation", "Arc authority token delegation does not match.");
|
|
300
|
+
}
|
|
301
|
+
if (options.expectedOrgId && claims.org_id !== options.expectedOrgId) {
|
|
302
|
+
throw new ArcRequestVerificationError("unexpected_org", "Arc authority token organization does not match.");
|
|
303
|
+
}
|
|
304
|
+
if (options.expectedWorkspaceId && claims.workspace_id !== options.expectedWorkspaceId) {
|
|
305
|
+
throw new ArcRequestVerificationError("unexpected_workspace", "Arc authority token workspace does not match.");
|
|
306
|
+
}
|
|
307
|
+
if (options.expectedDecision && claims.decision !== options.expectedDecision) {
|
|
308
|
+
throw new ArcRequestVerificationError("unexpected_decision", "Arc authority token decision does not match.");
|
|
309
|
+
}
|
|
310
|
+
if (options.nonceStore) {
|
|
311
|
+
const accepted = await options.nonceStore.useNonce(claims.nonce, new Date(claims.exp * 1000));
|
|
312
|
+
if (!accepted) {
|
|
313
|
+
throw new ArcRequestVerificationError("nonce_replayed", "Arc authority token nonce has already been used.");
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return {
|
|
317
|
+
claims,
|
|
318
|
+
expiresAt: new Date(claims.exp * 1000),
|
|
319
|
+
valid: true
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
export async function requireArcAuthority(token, options) {
|
|
323
|
+
const verified = await verifyArcAuthorityToken(token, options);
|
|
324
|
+
return verified.claims;
|
|
325
|
+
}
|
|
326
|
+
export function createArcAuthorityMiddleware(options) {
|
|
327
|
+
return async (request, response, next) => {
|
|
328
|
+
try {
|
|
329
|
+
const token = options.tokenResolver?.(request) ?? readAuthorityTokenFromRequest(request);
|
|
330
|
+
const expectedActionKey = options.actionResolver?.(request) ?? options.expectedActionKey;
|
|
331
|
+
if (!token) {
|
|
332
|
+
throw new ArcRequestVerificationError("missing_authority", "Arc authority token is required.");
|
|
333
|
+
}
|
|
334
|
+
const verified = await verifyArcAuthorityToken(token, {
|
|
335
|
+
...options,
|
|
336
|
+
expectedActionKey
|
|
337
|
+
});
|
|
338
|
+
attachArcAuthority(request, verified.claims);
|
|
339
|
+
if (typeof next === "function") {
|
|
340
|
+
next();
|
|
341
|
+
return undefined;
|
|
342
|
+
}
|
|
343
|
+
return verified.claims;
|
|
344
|
+
}
|
|
345
|
+
catch (error) {
|
|
346
|
+
if (typeof next === "function") {
|
|
347
|
+
next(error);
|
|
348
|
+
return undefined;
|
|
349
|
+
}
|
|
350
|
+
if (response && typeof response === "object" && "status" in response) {
|
|
351
|
+
const status = response.status;
|
|
352
|
+
if (typeof status === "function") {
|
|
353
|
+
const statusResult = status.call(response, 401);
|
|
354
|
+
const json = isPlainObject(statusResult) ? statusResult.json : undefined;
|
|
355
|
+
if (typeof json === "function") {
|
|
356
|
+
return json.call(statusResult, {
|
|
357
|
+
error: {
|
|
358
|
+
code: error instanceof ArcRequestVerificationError ? error.code : "authority_rejected",
|
|
359
|
+
message: "Arc authority was rejected."
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
throw error;
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
export async function introspectArcAuthority(input) {
|
|
370
|
+
const fetchImpl = input.fetch ?? fetch;
|
|
371
|
+
const response = await requestJson({
|
|
372
|
+
baseUrl: input.apiUrl,
|
|
373
|
+
body: {
|
|
374
|
+
invocation_id: input.invocationId,
|
|
375
|
+
signed_execution_id: input.signedExecutionId,
|
|
376
|
+
token: input.token
|
|
377
|
+
},
|
|
378
|
+
fetchImpl,
|
|
379
|
+
headers: {
|
|
380
|
+
authorization: `Bearer ${input.appApiKey}`
|
|
381
|
+
},
|
|
382
|
+
method: "POST",
|
|
383
|
+
path: "/v1/authority/introspect",
|
|
384
|
+
retrySafeGet: false
|
|
385
|
+
});
|
|
386
|
+
return response.value.data.introspection;
|
|
387
|
+
}
|
|
388
|
+
export function verifyArcDelegation(payload, options = {}) {
|
|
389
|
+
const requireActiveDelegation = options.requireActiveDelegation ?? true;
|
|
390
|
+
const requireApprovedAsk = options.requireApprovedAsk ?? true;
|
|
391
|
+
const authorization = payload.authorization;
|
|
392
|
+
if (!authorization) {
|
|
393
|
+
if (options.expectedDelegationId) {
|
|
394
|
+
throw new ArcRequestVerificationError("unexpected_delegation", "Arc invocation delegation does not match.");
|
|
395
|
+
}
|
|
396
|
+
if (requireActiveDelegation) {
|
|
397
|
+
throw new ArcRequestVerificationError("missing_delegation_context", "Arc delegation context is required.");
|
|
398
|
+
}
|
|
399
|
+
return true;
|
|
400
|
+
}
|
|
401
|
+
if (authorization.issued_by !== "arc" || authorization.decision !== payload.decision) {
|
|
402
|
+
throw new ArcRequestVerificationError("invalid_delegation_context", "Arc delegation context is invalid.");
|
|
403
|
+
}
|
|
404
|
+
if (requireActiveDelegation &&
|
|
405
|
+
(!authorization.delegation_id || authorization.delegation_status !== "active")) {
|
|
406
|
+
throw new ArcRequestVerificationError("delegation_not_active", "Arc delegation is not active.");
|
|
407
|
+
}
|
|
408
|
+
if (options.expectedDelegationId && authorization.delegation_id !== options.expectedDelegationId) {
|
|
409
|
+
throw new ArcRequestVerificationError("unexpected_delegation", "Arc invocation delegation does not match.");
|
|
410
|
+
}
|
|
411
|
+
if (requireApprovedAsk && payload.decision === "ask" && authorization.approval_status !== "approved") {
|
|
412
|
+
throw new ArcRequestVerificationError("approval_not_valid", "Arc approval state is not valid for this action.");
|
|
413
|
+
}
|
|
414
|
+
return true;
|
|
415
|
+
}
|
|
416
|
+
export async function validateArcInvocation(payload, options = {}) {
|
|
417
|
+
if (options.expectedAppId && payload.app_id !== options.expectedAppId) {
|
|
418
|
+
throw new ArcRequestVerificationError("unexpected_app", "Arc invocation was signed for a different app.");
|
|
419
|
+
}
|
|
420
|
+
if (options.expectedActionKey && payload.action_key !== options.expectedActionKey) {
|
|
421
|
+
throw new ArcRequestVerificationError("unexpected_action", "Arc invocation was signed for a different action.");
|
|
422
|
+
}
|
|
423
|
+
if (options.expectedDecision && payload.decision !== options.expectedDecision) {
|
|
424
|
+
throw new ArcRequestVerificationError("unexpected_decision", "Arc invocation decision does not match.");
|
|
425
|
+
}
|
|
426
|
+
if (options.expectedRiskLevel && payload.risk_level !== options.expectedRiskLevel) {
|
|
427
|
+
throw new ArcRequestVerificationError("unexpected_risk", "Arc invocation risk level does not match.");
|
|
428
|
+
}
|
|
429
|
+
if (options.expectedOrgId && payload.org_id !== options.expectedOrgId) {
|
|
430
|
+
throw new ArcRequestVerificationError("unexpected_org", "Arc invocation organization does not match.");
|
|
431
|
+
}
|
|
432
|
+
if (options.expectedWorkspaceId && payload.workspace_id !== options.expectedWorkspaceId) {
|
|
433
|
+
throw new ArcRequestVerificationError("unexpected_workspace", "Arc invocation workspace does not match.");
|
|
434
|
+
}
|
|
435
|
+
if (options.inputSchema) {
|
|
436
|
+
validateInputAgainstSchema(payload.input, options.inputSchema);
|
|
437
|
+
}
|
|
438
|
+
if (options.invocationStore) {
|
|
439
|
+
const accepted = await options.invocationStore.useInvocation(payload.invocation_id);
|
|
440
|
+
if (!accepted) {
|
|
441
|
+
throw new ArcRequestVerificationError("invocation_replayed", "Arc invocation id has already been processed.");
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
return true;
|
|
445
|
+
}
|
|
446
|
+
export function createMemoryNonceStore() {
|
|
447
|
+
const seen = new Map();
|
|
448
|
+
return {
|
|
449
|
+
useNonce: (nonce, expiresAt) => {
|
|
450
|
+
const now = Date.now();
|
|
451
|
+
for (const [key, expires] of seen.entries()) {
|
|
452
|
+
if (expires <= now) {
|
|
453
|
+
seen.delete(key);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
if (seen.has(nonce)) {
|
|
457
|
+
return false;
|
|
458
|
+
}
|
|
459
|
+
seen.set(nonce, expiresAt.getTime());
|
|
460
|
+
return true;
|
|
461
|
+
}
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
export class ArcDeveloperClient {
|
|
465
|
+
options;
|
|
466
|
+
fetchImpl;
|
|
467
|
+
sessionCookie;
|
|
468
|
+
constructor(options = {}) {
|
|
469
|
+
this.options = options;
|
|
470
|
+
this.fetchImpl = options.fetch ?? fetch;
|
|
471
|
+
this.sessionCookie = options.sessionCookie ?? null;
|
|
472
|
+
}
|
|
473
|
+
get cookie() {
|
|
474
|
+
return this.sessionCookie;
|
|
475
|
+
}
|
|
476
|
+
setCookie(cookie) {
|
|
477
|
+
this.sessionCookie = cookie;
|
|
478
|
+
}
|
|
479
|
+
async devLogin(input) {
|
|
480
|
+
return this.request("POST", "/v1/auth/dev-login", {
|
|
481
|
+
email: input.email,
|
|
482
|
+
name: input.name ?? undefined
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
async logout() {
|
|
486
|
+
const response = await this.request("POST", "/v1/auth/logout", {});
|
|
487
|
+
this.sessionCookie = null;
|
|
488
|
+
return response;
|
|
489
|
+
}
|
|
490
|
+
async me() {
|
|
491
|
+
return this.request("GET", "/v1/me");
|
|
492
|
+
}
|
|
493
|
+
async listOrgs() {
|
|
494
|
+
return this.request("GET", "/v1/orgs");
|
|
495
|
+
}
|
|
496
|
+
async getCurrentOrg() {
|
|
497
|
+
return this.request("GET", "/v1/orgs/current");
|
|
498
|
+
}
|
|
499
|
+
async listOrgMembers() {
|
|
500
|
+
return this.request("GET", "/v1/orgs/members");
|
|
501
|
+
}
|
|
502
|
+
async inviteOrgMember(input) {
|
|
503
|
+
return this.request("POST", "/v1/users/invite", input);
|
|
504
|
+
}
|
|
505
|
+
async createApp(input) {
|
|
506
|
+
return this.request("POST", "/v1/apps", input);
|
|
507
|
+
}
|
|
508
|
+
async createAppFromManifest(input) {
|
|
509
|
+
return this.request("POST", "/v1/apps/manifest", input);
|
|
510
|
+
}
|
|
511
|
+
async listApps() {
|
|
512
|
+
return this.request("GET", "/v1/apps");
|
|
513
|
+
}
|
|
514
|
+
async getApp(appId) {
|
|
515
|
+
return this.request("GET", `/v1/apps/${encodeURIComponent(appId)}`);
|
|
516
|
+
}
|
|
517
|
+
async createAppApiKey(appId, input = {}) {
|
|
518
|
+
return this.request("POST", `/v1/apps/${encodeURIComponent(appId)}/api-keys`, input);
|
|
519
|
+
}
|
|
520
|
+
async listAppApiKeys(appId) {
|
|
521
|
+
return this.request("GET", `/v1/apps/${encodeURIComponent(appId)}/api-keys`);
|
|
522
|
+
}
|
|
523
|
+
async revokeAppApiKey(appId, keyId) {
|
|
524
|
+
return this.request("POST", `/v1/apps/${encodeURIComponent(appId)}/api-keys/${encodeURIComponent(keyId)}/revoke`, {});
|
|
525
|
+
}
|
|
526
|
+
async rotateAppApiKey(appId, keyId, input = {}) {
|
|
527
|
+
return this.request("POST", `/v1/apps/${encodeURIComponent(appId)}/api-keys/${encodeURIComponent(keyId)}/rotate`, input);
|
|
528
|
+
}
|
|
529
|
+
async syncActions(appId, actions) {
|
|
530
|
+
const payload = Array.isArray(actions) ? { actions } : createActionSyncPayload(actions);
|
|
531
|
+
return this.request("PUT", `/v1/apps/${encodeURIComponent(appId)}/actions`, payload);
|
|
532
|
+
}
|
|
533
|
+
async createAppConnection(input) {
|
|
534
|
+
return this.request("POST", "/v1/app-connections", input);
|
|
535
|
+
}
|
|
536
|
+
async createDevAgentToken(input) {
|
|
537
|
+
return this.request("POST", "/v1/agents/dev-token", {
|
|
538
|
+
agent_type: input.agent_type,
|
|
539
|
+
display_name: input.display_name ?? undefined
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
async registerAgent(input) {
|
|
543
|
+
return this.request("POST", "/v1/agents/register", input);
|
|
544
|
+
}
|
|
545
|
+
async listAgents() {
|
|
546
|
+
return this.request("GET", "/v1/agents");
|
|
547
|
+
}
|
|
548
|
+
async getAgent(agentId) {
|
|
549
|
+
return this.request("GET", `/v1/agents/${encodeURIComponent(agentId)}`);
|
|
550
|
+
}
|
|
551
|
+
async updateAgent(agentId, input) {
|
|
552
|
+
return this.request("PATCH", `/v1/agents/${encodeURIComponent(agentId)}`, input);
|
|
553
|
+
}
|
|
554
|
+
async revokeAgent(agentId) {
|
|
555
|
+
return this.request("POST", `/v1/agents/${encodeURIComponent(agentId)}/revoke`, {});
|
|
556
|
+
}
|
|
557
|
+
async regenerateAgentToken(agentId, input = {}) {
|
|
558
|
+
return this.request("POST", `/v1/agents/${encodeURIComponent(agentId)}/regenerate-token`, input);
|
|
559
|
+
}
|
|
560
|
+
async listAgentGrants(agentId) {
|
|
561
|
+
return this.request("GET", `/v1/agents/${encodeURIComponent(agentId)}/grants`);
|
|
562
|
+
}
|
|
563
|
+
async putAgentGrant(agentId, appId, actions) {
|
|
564
|
+
return this.request("PUT", `/v1/agents/${encodeURIComponent(agentId)}/grants/${encodeURIComponent(appId)}`, { actions });
|
|
565
|
+
}
|
|
566
|
+
async listDelegations() {
|
|
567
|
+
return this.request("GET", "/v1/delegations");
|
|
568
|
+
}
|
|
569
|
+
async getDelegation(delegationId) {
|
|
570
|
+
return this.request("GET", `/v1/delegations/${encodeURIComponent(delegationId)}`);
|
|
571
|
+
}
|
|
572
|
+
async createDelegation(input) {
|
|
573
|
+
return this.request("POST", "/v1/delegations", input);
|
|
574
|
+
}
|
|
575
|
+
async updateDelegationPermissions(delegationId, input) {
|
|
576
|
+
return this.request("PATCH", `/v1/delegations/${encodeURIComponent(delegationId)}`, input);
|
|
577
|
+
}
|
|
578
|
+
async revokeDelegation(delegationId, input = {}) {
|
|
579
|
+
return this.request("POST", `/v1/delegations/${encodeURIComponent(delegationId)}/revoke`, input);
|
|
580
|
+
}
|
|
581
|
+
async listDelegationAudit(delegationId) {
|
|
582
|
+
return this.request("GET", `/v1/delegations/${encodeURIComponent(delegationId)}/audit`);
|
|
583
|
+
}
|
|
584
|
+
async listRegistryApps() {
|
|
585
|
+
return this.request("GET", "/v1/registry/apps");
|
|
586
|
+
}
|
|
587
|
+
async getRegistryApp(appId) {
|
|
588
|
+
return this.request("GET", `/v1/registry/apps/${encodeURIComponent(appId)}`);
|
|
589
|
+
}
|
|
590
|
+
async listRegistryAppActions(appId) {
|
|
591
|
+
return this.request("GET", `/v1/registry/apps/${encodeURIComponent(appId)}/actions`);
|
|
592
|
+
}
|
|
593
|
+
async getRegistryAppManifest(appId) {
|
|
594
|
+
return this.request("GET", `/v1/registry/apps/${encodeURIComponent(appId)}/manifest`);
|
|
595
|
+
}
|
|
596
|
+
async listApprovals() {
|
|
597
|
+
return this.request("GET", "/v1/approvals");
|
|
598
|
+
}
|
|
599
|
+
async getApproval(approvalId) {
|
|
600
|
+
return this.request("GET", `/v1/approvals/${encodeURIComponent(approvalId)}`);
|
|
601
|
+
}
|
|
602
|
+
async approveApproval(approvalId) {
|
|
603
|
+
return this.request("POST", `/v1/approvals/${encodeURIComponent(approvalId)}/approve`, {});
|
|
604
|
+
}
|
|
605
|
+
async denyApproval(approvalId) {
|
|
606
|
+
return this.request("POST", `/v1/approvals/${encodeURIComponent(approvalId)}/deny`, {});
|
|
607
|
+
}
|
|
608
|
+
async tailAudit(input = {}) {
|
|
609
|
+
const limit = input.limit ?? 20;
|
|
610
|
+
return this.request("GET", `/v1/audit?limit=${encodeURIComponent(String(limit))}`);
|
|
611
|
+
}
|
|
612
|
+
async exportAudit(input = {}) {
|
|
613
|
+
return this.request("GET", `/v1/audit/export${auditQueryString(input)}`);
|
|
614
|
+
}
|
|
615
|
+
async createAuditExport(input = {}) {
|
|
616
|
+
return this.request("POST", "/v1/audit/export", input);
|
|
617
|
+
}
|
|
618
|
+
async getAuditExport(exportId) {
|
|
619
|
+
return this.request("GET", `/v1/audit/export/${encodeURIComponent(exportId)}`);
|
|
620
|
+
}
|
|
621
|
+
async verifyAudit(input = {}) {
|
|
622
|
+
return this.request("GET", `/v1/audit/verify${auditQueryString(input)}`);
|
|
623
|
+
}
|
|
624
|
+
async getAuditTimeline(input = {}) {
|
|
625
|
+
return this.request("GET", `/v1/audit/timeline${auditQueryString(input)}`);
|
|
626
|
+
}
|
|
627
|
+
async getJwks() {
|
|
628
|
+
return this.request("GET", "/.well-known/jwks.json");
|
|
629
|
+
}
|
|
630
|
+
async getAuthorityMetadata() {
|
|
631
|
+
return this.request("GET", "/.well-known/arc-configuration");
|
|
632
|
+
}
|
|
633
|
+
async introspectAuthority(input) {
|
|
634
|
+
return this.request("POST", "/v1/authority/introspect", input);
|
|
635
|
+
}
|
|
636
|
+
async request(method, path, body, options = {}) {
|
|
637
|
+
const response = await requestJson({
|
|
638
|
+
baseUrl: this.options.baseUrl,
|
|
639
|
+
body,
|
|
640
|
+
fetchImpl: this.fetchImpl,
|
|
641
|
+
headers: {
|
|
642
|
+
...this.authHeaders(),
|
|
643
|
+
...options.headers
|
|
644
|
+
},
|
|
645
|
+
method,
|
|
646
|
+
path,
|
|
647
|
+
retrySafeGet: options.retrySafeGet ?? method.toUpperCase() === "GET"
|
|
648
|
+
});
|
|
649
|
+
const setCookie = response.setCookie;
|
|
650
|
+
if (setCookie) {
|
|
651
|
+
this.sessionCookie = setCookie;
|
|
652
|
+
}
|
|
653
|
+
return response.value;
|
|
654
|
+
}
|
|
655
|
+
authHeaders() {
|
|
656
|
+
const headers = {};
|
|
657
|
+
if (this.sessionCookie) {
|
|
658
|
+
headers.cookie = this.sessionCookie;
|
|
659
|
+
}
|
|
660
|
+
if (this.options.apiKey) {
|
|
661
|
+
headers.authorization = `Bearer ${this.options.apiKey}`;
|
|
662
|
+
}
|
|
663
|
+
if (this.options.orgId) {
|
|
664
|
+
headers["x-arc-org-id"] = this.options.orgId;
|
|
665
|
+
}
|
|
666
|
+
return headers;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
export class ArcAgentRuntimeClient {
|
|
670
|
+
options;
|
|
671
|
+
fetchImpl;
|
|
672
|
+
constructor(options = {}) {
|
|
673
|
+
this.options = options;
|
|
674
|
+
this.fetchImpl = options.fetch ?? fetch;
|
|
675
|
+
}
|
|
676
|
+
async me() {
|
|
677
|
+
const response = await this.request("GET", "/v1/agent/me");
|
|
678
|
+
return response.data.agent;
|
|
679
|
+
}
|
|
680
|
+
async listApps() {
|
|
681
|
+
const response = await this.request("GET", "/v1/agent/apps");
|
|
682
|
+
return response.data.apps;
|
|
683
|
+
}
|
|
684
|
+
async listActions(appId) {
|
|
685
|
+
const response = await this.request("GET", `/v1/agent/apps/${encodeURIComponent(appId)}/actions`);
|
|
686
|
+
return response.data.actions;
|
|
687
|
+
}
|
|
688
|
+
async getAvailableActions() {
|
|
689
|
+
const response = await this.request("GET", "/v1/agent/actions");
|
|
690
|
+
return response.data.actions;
|
|
691
|
+
}
|
|
692
|
+
async createActionRequest(input) {
|
|
693
|
+
return this.request("POST", "/v1/action-requests", input);
|
|
694
|
+
}
|
|
695
|
+
async pollActionRequest(actionRequestId) {
|
|
696
|
+
return this.request("GET", `/v1/action-requests/${encodeURIComponent(actionRequestId)}`);
|
|
697
|
+
}
|
|
698
|
+
async listGrants() {
|
|
699
|
+
const response = await this.request("GET", "/v1/agent/grants");
|
|
700
|
+
return response.data.grants;
|
|
701
|
+
}
|
|
702
|
+
async invoke(appId, actionKey, input = {}, options = {}) {
|
|
703
|
+
const idempotencyKey = options.idempotencyKey ?? randomUUID();
|
|
704
|
+
try {
|
|
705
|
+
const response = await this.request("POST", "/v1/agent/invocations", {
|
|
706
|
+
action_key: actionKey,
|
|
707
|
+
app_id: appId,
|
|
708
|
+
idempotency_key: idempotencyKey,
|
|
709
|
+
input
|
|
710
|
+
});
|
|
711
|
+
return parseRuntimeInvocationResult(response, idempotencyKey);
|
|
712
|
+
}
|
|
713
|
+
catch (error) {
|
|
714
|
+
if (error instanceof ArcHttpError) {
|
|
715
|
+
return runtimeErrorResult({
|
|
716
|
+
code: error.code,
|
|
717
|
+
idempotencyKey,
|
|
718
|
+
message: error.message,
|
|
719
|
+
raw: error.responseBody,
|
|
720
|
+
status: error.status
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
if (error instanceof Error) {
|
|
724
|
+
return runtimeErrorResult({
|
|
725
|
+
code: "request_failed",
|
|
726
|
+
idempotencyKey,
|
|
727
|
+
message: error.message,
|
|
728
|
+
raw: null
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
return runtimeErrorResult({
|
|
732
|
+
code: "request_failed",
|
|
733
|
+
idempotencyKey,
|
|
734
|
+
message: "Arc request failed.",
|
|
735
|
+
raw: null
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
async request(method, path, body) {
|
|
740
|
+
const agentToken = this.options.agentToken;
|
|
741
|
+
if (!agentToken) {
|
|
742
|
+
throw new ArcValidationError("missing_agent_token", "Arc agent token is required.");
|
|
743
|
+
}
|
|
744
|
+
const response = await requestJson({
|
|
745
|
+
baseUrl: this.options.apiUrl,
|
|
746
|
+
body,
|
|
747
|
+
fetchImpl: this.fetchImpl,
|
|
748
|
+
headers: {
|
|
749
|
+
authorization: `Bearer ${agentToken}`
|
|
750
|
+
},
|
|
751
|
+
method,
|
|
752
|
+
path,
|
|
753
|
+
retrySafeGet: method.toUpperCase() === "GET"
|
|
754
|
+
});
|
|
755
|
+
return response.value;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
export function createArcAgentRuntime(options) {
|
|
759
|
+
return new ArcAgentRuntimeClient(options);
|
|
760
|
+
}
|
|
761
|
+
export class ArcAgentClient {
|
|
762
|
+
options;
|
|
763
|
+
fetchImpl;
|
|
764
|
+
constructor(options = {}) {
|
|
765
|
+
this.options = options;
|
|
766
|
+
this.fetchImpl = options.fetch ?? fetch;
|
|
767
|
+
}
|
|
768
|
+
async invoke(input) {
|
|
769
|
+
const idempotencyKey = input.idempotency_key ?? randomUUID();
|
|
770
|
+
return this.request("POST", "/v1/agent/invocations", {
|
|
771
|
+
action_key: input.action_key,
|
|
772
|
+
app_id: input.app_id,
|
|
773
|
+
app_slug: input.app_slug,
|
|
774
|
+
idempotency_key: idempotencyKey,
|
|
775
|
+
input: input.input ?? {}
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
async listActions(input) {
|
|
779
|
+
const params = new URLSearchParams();
|
|
780
|
+
if (input.app_id) {
|
|
781
|
+
params.set("app_id", input.app_id);
|
|
782
|
+
}
|
|
783
|
+
if (input.app_slug) {
|
|
784
|
+
params.set("app_slug", input.app_slug);
|
|
785
|
+
}
|
|
786
|
+
return this.request("GET", `/v1/agent/actions?${params.toString()}`);
|
|
787
|
+
}
|
|
788
|
+
async createActionRequest(input) {
|
|
789
|
+
return this.request("POST", "/v1/action-requests", input);
|
|
790
|
+
}
|
|
791
|
+
async pollActionRequest(actionRequestId) {
|
|
792
|
+
return this.request("GET", `/v1/action-requests/${encodeURIComponent(actionRequestId)}`);
|
|
793
|
+
}
|
|
794
|
+
async getInvocation(invocationId) {
|
|
795
|
+
return this.request("GET", `/v1/agent/invocations/${encodeURIComponent(invocationId)}`);
|
|
796
|
+
}
|
|
797
|
+
async me() {
|
|
798
|
+
return createArcAgentRuntime({
|
|
799
|
+
agentToken: this.options.agentToken,
|
|
800
|
+
apiUrl: this.options.baseUrl,
|
|
801
|
+
fetch: this.options.fetch
|
|
802
|
+
}).me();
|
|
803
|
+
}
|
|
804
|
+
async listApps() {
|
|
805
|
+
return createArcAgentRuntime({
|
|
806
|
+
agentToken: this.options.agentToken,
|
|
807
|
+
apiUrl: this.options.baseUrl,
|
|
808
|
+
fetch: this.options.fetch
|
|
809
|
+
}).listApps();
|
|
810
|
+
}
|
|
811
|
+
async listGrants() {
|
|
812
|
+
return createArcAgentRuntime({
|
|
813
|
+
agentToken: this.options.agentToken,
|
|
814
|
+
apiUrl: this.options.baseUrl,
|
|
815
|
+
fetch: this.options.fetch
|
|
816
|
+
}).listGrants();
|
|
817
|
+
}
|
|
818
|
+
async request(method, path, body) {
|
|
819
|
+
const agentToken = this.options.agentToken;
|
|
820
|
+
if (!agentToken) {
|
|
821
|
+
throw new ArcValidationError("missing_agent_token", "Arc agent token is required.");
|
|
822
|
+
}
|
|
823
|
+
const response = await requestJson({
|
|
824
|
+
baseUrl: this.options.baseUrl,
|
|
825
|
+
body,
|
|
826
|
+
fetchImpl: this.fetchImpl,
|
|
827
|
+
headers: {
|
|
828
|
+
authorization: `Bearer ${agentToken}`
|
|
829
|
+
},
|
|
830
|
+
method,
|
|
831
|
+
path,
|
|
832
|
+
retrySafeGet: method.toUpperCase() === "GET"
|
|
833
|
+
});
|
|
834
|
+
return response.value;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
export const arc = {
|
|
838
|
+
ArcAgentClient,
|
|
839
|
+
ArcAgentRuntimeClient,
|
|
840
|
+
ArcDeveloperClient,
|
|
841
|
+
createActionSyncPayload,
|
|
842
|
+
createArcAuthorityMiddleware,
|
|
843
|
+
createArcExecutor,
|
|
844
|
+
createArcAgentRuntime,
|
|
845
|
+
createMemoryNonceStore,
|
|
846
|
+
defineActions,
|
|
847
|
+
handleAction,
|
|
848
|
+
introspectArcAuthority,
|
|
849
|
+
requireArcAuthority,
|
|
850
|
+
syncActions,
|
|
851
|
+
validateArcInvocation,
|
|
852
|
+
verifyArcDelegation,
|
|
853
|
+
verifyArcExecution,
|
|
854
|
+
verifyArcSignature,
|
|
855
|
+
verifyArcAuthorityToken,
|
|
856
|
+
verifyArcRequest
|
|
857
|
+
};
|
|
858
|
+
function parseRuntimeInvocationResult(response, idempotencyKey) {
|
|
859
|
+
const envelope = isPlainObject(response) ? response : {};
|
|
860
|
+
const data = isPlainObject(envelope.data) ? envelope.data : {};
|
|
861
|
+
const invocation = isPlainObject(data.invocation) ? data.invocation : {};
|
|
862
|
+
const approval = isPlainObject(data.approval) ? data.approval : {};
|
|
863
|
+
const decisionRecord = isPlainObject(data.decision) ? data.decision : {};
|
|
864
|
+
const decision = typeof decisionRecord.decision === "string"
|
|
865
|
+
? decisionRecord.decision
|
|
866
|
+
: typeof invocation.decision === "string"
|
|
867
|
+
? invocation.decision
|
|
868
|
+
: null;
|
|
869
|
+
const status = typeof data.status === "string"
|
|
870
|
+
? data.status
|
|
871
|
+
: typeof invocation.status === "string"
|
|
872
|
+
? invocation.status
|
|
873
|
+
: null;
|
|
874
|
+
const invocationId = typeof invocation.id === "string" ? invocation.id : null;
|
|
875
|
+
const approvalId = typeof approval.id === "string" ? approval.id : null;
|
|
876
|
+
if (decision === "ask") {
|
|
877
|
+
return {
|
|
878
|
+
approval_id: approvalId,
|
|
879
|
+
decision,
|
|
880
|
+
idempotency_key: idempotencyKey,
|
|
881
|
+
invocation_id: invocationId,
|
|
882
|
+
raw: response,
|
|
883
|
+
result: null,
|
|
884
|
+
status: "pending_approval"
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
if (decision === "block") {
|
|
888
|
+
return {
|
|
889
|
+
approval_id: null,
|
|
890
|
+
decision,
|
|
891
|
+
idempotency_key: idempotencyKey,
|
|
892
|
+
invocation_id: invocationId,
|
|
893
|
+
raw: response,
|
|
894
|
+
result: null,
|
|
895
|
+
status: "blocked"
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
if (decision === "allow" && status === "queued_for_execution") {
|
|
899
|
+
return {
|
|
900
|
+
approval_id: null,
|
|
901
|
+
decision,
|
|
902
|
+
idempotency_key: idempotencyKey,
|
|
903
|
+
invocation_id: invocationId,
|
|
904
|
+
raw: response,
|
|
905
|
+
result: null,
|
|
906
|
+
status: "queued"
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
if (decision === "allow" && status === "execution_succeeded") {
|
|
910
|
+
return {
|
|
911
|
+
approval_id: null,
|
|
912
|
+
decision,
|
|
913
|
+
idempotency_key: idempotencyKey,
|
|
914
|
+
invocation_id: invocationId,
|
|
915
|
+
raw: response,
|
|
916
|
+
result: invocation.result_redacted ?? null,
|
|
917
|
+
status: "executed"
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
return runtimeErrorResult({
|
|
921
|
+
code: "invalid_arc_response",
|
|
922
|
+
idempotencyKey,
|
|
923
|
+
message: "Arc returned an unknown invocation decision or status.",
|
|
924
|
+
raw: response,
|
|
925
|
+
decision,
|
|
926
|
+
invocationId
|
|
927
|
+
});
|
|
928
|
+
}
|
|
929
|
+
function runtimeErrorResult(input) {
|
|
930
|
+
return {
|
|
931
|
+
approval_id: null,
|
|
932
|
+
decision: input.decision ?? null,
|
|
933
|
+
error: {
|
|
934
|
+
code: input.code,
|
|
935
|
+
message: input.message,
|
|
936
|
+
...(input.status === undefined ? {} : { status: input.status })
|
|
937
|
+
},
|
|
938
|
+
idempotency_key: input.idempotencyKey,
|
|
939
|
+
invocation_id: input.invocationId ?? null,
|
|
940
|
+
raw: input.raw,
|
|
941
|
+
result: null,
|
|
942
|
+
status: "error"
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
function auditQueryString(input) {
|
|
946
|
+
const params = new URLSearchParams();
|
|
947
|
+
const filters = input.filters ?? input;
|
|
948
|
+
const entries = [
|
|
949
|
+
["action_id", filters.action_id],
|
|
950
|
+
["action_key", filters.action_key],
|
|
951
|
+
["actor_id", filters.actor_id],
|
|
952
|
+
["agent_id", filters.agent_id],
|
|
953
|
+
["app_id", filters.app_id],
|
|
954
|
+
["approval_id", filters.approval_id],
|
|
955
|
+
["decision", filters.decision],
|
|
956
|
+
["delegation_id", filters.delegation_id],
|
|
957
|
+
["event_type", filters.event_type],
|
|
958
|
+
["from", filters.from],
|
|
959
|
+
["invocation_id", filters.invocation_id],
|
|
960
|
+
["status", filters.status],
|
|
961
|
+
["to", filters.to],
|
|
962
|
+
["user_id", filters.user_id],
|
|
963
|
+
["format", input.format],
|
|
964
|
+
["limit", input.limit],
|
|
965
|
+
["offset", input.offset]
|
|
966
|
+
];
|
|
967
|
+
for (const [key, value] of entries) {
|
|
968
|
+
if (value !== undefined && value !== null && value !== "") {
|
|
969
|
+
params.set(key, String(value));
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
const queryString = params.toString();
|
|
973
|
+
return queryString ? `?${queryString}` : "";
|
|
974
|
+
}
|
|
975
|
+
async function requestJson(input) {
|
|
976
|
+
const maxAttempts = input.retrySafeGet ? 2 : 1;
|
|
977
|
+
let lastError = null;
|
|
978
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
979
|
+
try {
|
|
980
|
+
const response = await input.fetchImpl(buildUrl(input.baseUrl, input.path), {
|
|
981
|
+
body: input.body === undefined ? undefined : JSON.stringify(input.body),
|
|
982
|
+
headers: {
|
|
983
|
+
accept: "application/json",
|
|
984
|
+
...(input.body === undefined ? {} : { "content-type": "application/json" }),
|
|
985
|
+
...input.headers
|
|
986
|
+
},
|
|
987
|
+
method: input.method
|
|
988
|
+
});
|
|
989
|
+
if (response.status >= 500 && attempt < maxAttempts) {
|
|
990
|
+
continue;
|
|
991
|
+
}
|
|
992
|
+
const text = await response.text();
|
|
993
|
+
const value = text ? parseJsonResponse(text) : null;
|
|
994
|
+
const setCookie = parseSetCookie(response.headers);
|
|
995
|
+
if (!response.ok) {
|
|
996
|
+
const serverError = asServerError(value);
|
|
997
|
+
throw new ArcHttpError(response.status, serverError.code, serverError.message, value);
|
|
998
|
+
}
|
|
999
|
+
return {
|
|
1000
|
+
setCookie,
|
|
1001
|
+
value: value
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
catch (error) {
|
|
1005
|
+
lastError = error;
|
|
1006
|
+
if (error instanceof ArcHttpError || attempt >= maxAttempts) {
|
|
1007
|
+
throw error;
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
throw lastError instanceof Error
|
|
1012
|
+
? lastError
|
|
1013
|
+
: new ArcError("request_failed", "Arc request failed.");
|
|
1014
|
+
}
|
|
1015
|
+
function validateActionDefinitions(actions) {
|
|
1016
|
+
if (!isPlainObject(actions)) {
|
|
1017
|
+
throw new ArcValidationError("invalid_actions", "Arc actions must be an object.");
|
|
1018
|
+
}
|
|
1019
|
+
for (const [key, action] of Object.entries(actions)) {
|
|
1020
|
+
if (!actionKeyPattern.test(key)) {
|
|
1021
|
+
throw new ArcValidationError("invalid_action_key", `Arc action key is invalid: ${key}`);
|
|
1022
|
+
}
|
|
1023
|
+
if (!isPlainObject(action)) {
|
|
1024
|
+
throw new ArcValidationError("invalid_action", `Arc action ${key} must be an object.`);
|
|
1025
|
+
}
|
|
1026
|
+
if (typeof action.name !== "string" || action.name.trim().length === 0) {
|
|
1027
|
+
throw new ArcValidationError("invalid_action", `Arc action ${key} requires a name.`);
|
|
1028
|
+
}
|
|
1029
|
+
if (!riskLevels.has(action.risk)) {
|
|
1030
|
+
throw new ArcValidationError("invalid_risk", `Arc action ${key} has an invalid risk.`);
|
|
1031
|
+
}
|
|
1032
|
+
if (!defaultDecisions.has(action.defaultDecision)) {
|
|
1033
|
+
throw new ArcValidationError("invalid_default_decision", `Arc action ${key} has an invalid defaultDecision.`);
|
|
1034
|
+
}
|
|
1035
|
+
if (!isPlainObject(action.input)) {
|
|
1036
|
+
throw new ArcValidationError("invalid_input_schema", `Arc action ${key} requires an input schema object.`);
|
|
1037
|
+
}
|
|
1038
|
+
if (action.output !== undefined && !isPlainObject(action.output)) {
|
|
1039
|
+
throw new ArcValidationError("invalid_output_schema", `Arc action ${key} output must be a schema object.`);
|
|
1040
|
+
}
|
|
1041
|
+
if (action.tags !== undefined &&
|
|
1042
|
+
(!Array.isArray(action.tags) || !action.tags.every((tag) => typeof tag === "string" && tag.trim().length > 0))) {
|
|
1043
|
+
throw new ArcValidationError("invalid_tags", `Arc action ${key} tags must be non-empty strings.`);
|
|
1044
|
+
}
|
|
1045
|
+
if (action.description !== undefined && typeof action.description !== "string") {
|
|
1046
|
+
throw new ArcValidationError("invalid_action", `Arc action ${key} description must be a string.`);
|
|
1047
|
+
}
|
|
1048
|
+
if (action.enabled !== undefined && typeof action.enabled !== "boolean") {
|
|
1049
|
+
throw new ArcValidationError("invalid_action", `Arc action ${key} enabled must be a boolean.`);
|
|
1050
|
+
}
|
|
1051
|
+
if (action.cost !== undefined) {
|
|
1052
|
+
validateActionCost(key, action.cost);
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
function validateActionCost(key, cost) {
|
|
1057
|
+
if (!isPlainObject(cost)) {
|
|
1058
|
+
throw new ArcValidationError("invalid_cost", `Arc action ${key} cost must be an object.`);
|
|
1059
|
+
}
|
|
1060
|
+
if (cost.mode !== "fixed" && cost.mode !== "field") {
|
|
1061
|
+
throw new ArcValidationError("invalid_cost", `Arc action ${key} cost.mode must be "fixed" or "field".`);
|
|
1062
|
+
}
|
|
1063
|
+
if (cost.currency !== undefined && (typeof cost.currency !== "string" || !/^[A-Za-z]{3}$/.test(cost.currency))) {
|
|
1064
|
+
throw new ArcValidationError("invalid_cost", `Arc action ${key} cost.currency must be a 3-letter ISO code.`);
|
|
1065
|
+
}
|
|
1066
|
+
if (cost.mode === "fixed") {
|
|
1067
|
+
if (!Number.isInteger(cost.fixedMinor) || cost.fixedMinor < 0) {
|
|
1068
|
+
throw new ArcValidationError("invalid_cost", `Arc action ${key} cost.fixedMinor must be a non-negative integer (minor units, e.g. cents).`);
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
else if (typeof cost.field !== "string" || cost.field.trim().length === 0) {
|
|
1072
|
+
throw new ArcValidationError("invalid_cost", `Arc action ${key} cost.field must be a non-empty input field name.`);
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
function validateInputAgainstSchema(input, schema) {
|
|
1076
|
+
if (schema.type !== undefined && typeof schema.type === "string" && !matchesJsonSchemaType(input, schema.type)) {
|
|
1077
|
+
throw new ArcRequestVerificationError("invalid_input", "Arc invocation input does not match action schema.");
|
|
1078
|
+
}
|
|
1079
|
+
const required = schema.required;
|
|
1080
|
+
if (Array.isArray(required)) {
|
|
1081
|
+
if (!isPlainObject(input)) {
|
|
1082
|
+
throw new ArcRequestVerificationError("invalid_input", "Arc invocation input must be an object.");
|
|
1083
|
+
}
|
|
1084
|
+
for (const field of required) {
|
|
1085
|
+
if (typeof field === "string" && !(field in input)) {
|
|
1086
|
+
throw new ArcRequestVerificationError("invalid_input", "Arc invocation input is missing a required field.");
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
const properties = schema.properties;
|
|
1091
|
+
if (isPlainObject(properties) && isPlainObject(input)) {
|
|
1092
|
+
for (const [field, propertySchema] of Object.entries(properties)) {
|
|
1093
|
+
if (!(field in input) || !isPlainObject(propertySchema) || typeof propertySchema.type !== "string") {
|
|
1094
|
+
continue;
|
|
1095
|
+
}
|
|
1096
|
+
if (!matchesJsonSchemaType(input[field], propertySchema.type)) {
|
|
1097
|
+
throw new ArcRequestVerificationError("invalid_input", "Arc invocation input field does not match action schema.");
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
function matchesJsonSchemaType(value, type) {
|
|
1103
|
+
switch (type) {
|
|
1104
|
+
case "array":
|
|
1105
|
+
return Array.isArray(value);
|
|
1106
|
+
case "boolean":
|
|
1107
|
+
return typeof value === "boolean";
|
|
1108
|
+
case "integer":
|
|
1109
|
+
return Number.isInteger(value);
|
|
1110
|
+
case "null":
|
|
1111
|
+
return value === null;
|
|
1112
|
+
case "number":
|
|
1113
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
1114
|
+
case "object":
|
|
1115
|
+
return isPlainObject(value);
|
|
1116
|
+
case "string":
|
|
1117
|
+
return typeof value === "string";
|
|
1118
|
+
default:
|
|
1119
|
+
return true;
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
function buildUrl(baseUrl, path) {
|
|
1123
|
+
if (/^https?:\/\//u.test(path)) {
|
|
1124
|
+
return path;
|
|
1125
|
+
}
|
|
1126
|
+
return `${trimTrailingSlash(baseUrl ?? defaultBaseUrl)}${path.startsWith("/") ? path : `/${path}`}`;
|
|
1127
|
+
}
|
|
1128
|
+
function normalizeSlug(value) {
|
|
1129
|
+
return value
|
|
1130
|
+
.trim()
|
|
1131
|
+
.toLowerCase()
|
|
1132
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
1133
|
+
.replace(/^-+|-+$/g, "")
|
|
1134
|
+
.slice(0, 63);
|
|
1135
|
+
}
|
|
1136
|
+
export function createSlug(value) {
|
|
1137
|
+
const slug = normalizeSlug(value);
|
|
1138
|
+
if (slug.length >= 3 && slugPattern.test(slug)) {
|
|
1139
|
+
return slug;
|
|
1140
|
+
}
|
|
1141
|
+
return `app-${randomUUID().slice(0, 8)}`;
|
|
1142
|
+
}
|
|
1143
|
+
function parseProtectedHeader(signature) {
|
|
1144
|
+
try {
|
|
1145
|
+
return decodeProtectedHeader(signature);
|
|
1146
|
+
}
|
|
1147
|
+
catch {
|
|
1148
|
+
throw new ArcRequestVerificationError("invalid_signature", "Arc signature header is invalid.");
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
async function importVerificationKey(jwks, kid) {
|
|
1152
|
+
const keys = Array.isArray(jwks)
|
|
1153
|
+
? jwks
|
|
1154
|
+
: jwks && typeof jwks === "object" && Array.isArray(jwks.keys)
|
|
1155
|
+
? jwks.keys
|
|
1156
|
+
: null;
|
|
1157
|
+
if (!keys) {
|
|
1158
|
+
throw new ArcRequestVerificationError("jwks_invalid", "Arc JWKS response is invalid.");
|
|
1159
|
+
}
|
|
1160
|
+
const jwk = keys.find((item) => item && typeof item === "object" && item.kid === kid);
|
|
1161
|
+
if (!jwk) {
|
|
1162
|
+
throw new ArcRequestVerificationError("unknown_key", "Arc signing key is unknown.");
|
|
1163
|
+
}
|
|
1164
|
+
try {
|
|
1165
|
+
return await importJWK(jwk, "ES256");
|
|
1166
|
+
}
|
|
1167
|
+
catch {
|
|
1168
|
+
throw new ArcRequestVerificationError("invalid_signature", "Arc signing key is invalid.");
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
function readSignature(headers) {
|
|
1172
|
+
const authorization = readHeader(headers, "authorization");
|
|
1173
|
+
const match = /^Arc-Signature\s+(.+)$/i.exec(authorization ?? "");
|
|
1174
|
+
if (!match?.[1]) {
|
|
1175
|
+
throw new ArcRequestVerificationError("missing_signature", "Arc-Signature authorization header is required.");
|
|
1176
|
+
}
|
|
1177
|
+
return match[1].trim();
|
|
1178
|
+
}
|
|
1179
|
+
function readHeader(headers, name) {
|
|
1180
|
+
if (headers instanceof Headers) {
|
|
1181
|
+
return headers.get(name);
|
|
1182
|
+
}
|
|
1183
|
+
const pair = Object.entries(headers).find(([key]) => key.toLowerCase() === name.toLowerCase());
|
|
1184
|
+
const value = pair?.[1];
|
|
1185
|
+
if (Array.isArray(value)) {
|
|
1186
|
+
return value[0] ?? null;
|
|
1187
|
+
}
|
|
1188
|
+
return value ?? null;
|
|
1189
|
+
}
|
|
1190
|
+
function bodyToJson(body) {
|
|
1191
|
+
if (typeof body === "string") {
|
|
1192
|
+
return body;
|
|
1193
|
+
}
|
|
1194
|
+
if (body instanceof Uint8Array) {
|
|
1195
|
+
return new TextDecoder().decode(body);
|
|
1196
|
+
}
|
|
1197
|
+
return canonicalJson(body);
|
|
1198
|
+
}
|
|
1199
|
+
function parseBody(bodyJson) {
|
|
1200
|
+
let parsed;
|
|
1201
|
+
try {
|
|
1202
|
+
parsed = JSON.parse(bodyJson);
|
|
1203
|
+
}
|
|
1204
|
+
catch {
|
|
1205
|
+
throw new ArcRequestVerificationError("invalid_body", "Arc request body is invalid.");
|
|
1206
|
+
}
|
|
1207
|
+
if (!isPlainObject(parsed)) {
|
|
1208
|
+
throw new ArcRequestVerificationError("invalid_body", "Arc request body is invalid.");
|
|
1209
|
+
}
|
|
1210
|
+
const body = parsed;
|
|
1211
|
+
if (typeof body.invocation_id !== "string" ||
|
|
1212
|
+
typeof body.app_id !== "string" ||
|
|
1213
|
+
typeof body.action_key !== "string" ||
|
|
1214
|
+
typeof body.app_user_id !== "string" ||
|
|
1215
|
+
typeof body.org_id !== "string" ||
|
|
1216
|
+
typeof body.workspace_id !== "string" ||
|
|
1217
|
+
(body.decision !== "allow" && body.decision !== "ask") ||
|
|
1218
|
+
!riskLevels.has(body.risk_level) ||
|
|
1219
|
+
typeof body.input_hash !== "string" ||
|
|
1220
|
+
typeof body.timestamp !== "string" ||
|
|
1221
|
+
typeof body.nonce !== "string" ||
|
|
1222
|
+
!body.agent ||
|
|
1223
|
+
!body.user) {
|
|
1224
|
+
throw new ArcRequestVerificationError("invalid_body", "Arc request body is invalid.");
|
|
1225
|
+
}
|
|
1226
|
+
if (body.authorization !== undefined &&
|
|
1227
|
+
(!isPlainObject(body.authorization) ||
|
|
1228
|
+
body.authorization.issued_by !== "arc" ||
|
|
1229
|
+
(body.authorization.decision !== "allow" && body.authorization.decision !== "ask") ||
|
|
1230
|
+
(body.authorization.delegation_id !== null && typeof body.authorization.delegation_id !== "string") ||
|
|
1231
|
+
(body.authorization.delegation_status !== null && typeof body.authorization.delegation_status !== "string") ||
|
|
1232
|
+
(body.authorization.approval_status !== null && typeof body.authorization.approval_status !== "string"))) {
|
|
1233
|
+
throw new ArcRequestVerificationError("invalid_body", "Arc request authorization context is invalid.");
|
|
1234
|
+
}
|
|
1235
|
+
return body;
|
|
1236
|
+
}
|
|
1237
|
+
function parseClaims(value) {
|
|
1238
|
+
let parsed;
|
|
1239
|
+
try {
|
|
1240
|
+
parsed = JSON.parse(value);
|
|
1241
|
+
}
|
|
1242
|
+
catch {
|
|
1243
|
+
throw new ArcRequestVerificationError("invalid_signature", "Arc signature claims are invalid.");
|
|
1244
|
+
}
|
|
1245
|
+
if (!isPlainObject(parsed)) {
|
|
1246
|
+
throw new ArcRequestVerificationError("invalid_signature", "Arc signature claims are invalid.");
|
|
1247
|
+
}
|
|
1248
|
+
const claims = parsed;
|
|
1249
|
+
if (typeof claims.invocation_id !== "string" ||
|
|
1250
|
+
typeof claims.app_id !== "string" ||
|
|
1251
|
+
typeof claims.action_key !== "string" ||
|
|
1252
|
+
typeof claims.body_hash !== "string" ||
|
|
1253
|
+
typeof claims.input_hash !== "string" ||
|
|
1254
|
+
typeof claims.timestamp !== "string" ||
|
|
1255
|
+
typeof claims.org_id !== "string" ||
|
|
1256
|
+
typeof claims.workspace_id !== "string" ||
|
|
1257
|
+
typeof claims.nonce !== "string") {
|
|
1258
|
+
throw new ArcRequestVerificationError("invalid_signature", "Arc signature claims are invalid.");
|
|
1259
|
+
}
|
|
1260
|
+
return claims;
|
|
1261
|
+
}
|
|
1262
|
+
function parseAuthorityClaims(value) {
|
|
1263
|
+
let parsed;
|
|
1264
|
+
try {
|
|
1265
|
+
parsed = JSON.parse(value);
|
|
1266
|
+
}
|
|
1267
|
+
catch {
|
|
1268
|
+
throw new ArcRequestVerificationError("invalid_authority_token", "Arc authority token claims are invalid.");
|
|
1269
|
+
}
|
|
1270
|
+
if (!isPlainObject(parsed)) {
|
|
1271
|
+
throw new ArcRequestVerificationError("invalid_authority_token", "Arc authority token claims are invalid.");
|
|
1272
|
+
}
|
|
1273
|
+
const claims = parsed;
|
|
1274
|
+
if (claims.token_use !== "arc_authority" ||
|
|
1275
|
+
typeof claims.iss !== "string" ||
|
|
1276
|
+
typeof claims.sub !== "string" ||
|
|
1277
|
+
typeof claims.aud !== "string" ||
|
|
1278
|
+
typeof claims.scope !== "string" ||
|
|
1279
|
+
typeof claims.human_user_id !== "string" ||
|
|
1280
|
+
typeof claims.agent_id !== "string" ||
|
|
1281
|
+
typeof claims.org_id !== "string" ||
|
|
1282
|
+
typeof claims.workspace_id !== "string" ||
|
|
1283
|
+
typeof claims.invocation_id !== "string" ||
|
|
1284
|
+
typeof claims.app_id !== "string" ||
|
|
1285
|
+
typeof claims.action_key !== "string" ||
|
|
1286
|
+
typeof claims.body_hash !== "string" ||
|
|
1287
|
+
typeof claims.input_hash !== "string" ||
|
|
1288
|
+
typeof claims.timestamp !== "string" ||
|
|
1289
|
+
typeof claims.nonce !== "string" ||
|
|
1290
|
+
typeof claims.jti !== "string" ||
|
|
1291
|
+
typeof claims.iat !== "number" ||
|
|
1292
|
+
typeof claims.exp !== "number" ||
|
|
1293
|
+
(claims.decision !== "allow" && claims.decision !== "ask") ||
|
|
1294
|
+
(claims.delegation_id !== null && typeof claims.delegation_id !== "string") ||
|
|
1295
|
+
(claims.approval_id !== null && typeof claims.approval_id !== "string") ||
|
|
1296
|
+
!riskLevels.has(claims.risk_level) ||
|
|
1297
|
+
!["pending", "unverified", "verified", "official", "internal"].includes(String(claims.app_verification_status)) ||
|
|
1298
|
+
claims.aud !== claims.app_id ||
|
|
1299
|
+
claims.jti !== claims.invocation_id) {
|
|
1300
|
+
throw new ArcRequestVerificationError("invalid_authority_token", "Arc authority token claims are invalid.");
|
|
1301
|
+
}
|
|
1302
|
+
return claims;
|
|
1303
|
+
}
|
|
1304
|
+
function readAuthorityTokenFromRequest(request) {
|
|
1305
|
+
const headers = readRequestHeaders(request);
|
|
1306
|
+
const explicit = readHeader(headers, "x-arc-authority-token");
|
|
1307
|
+
if (explicit) {
|
|
1308
|
+
return explicit;
|
|
1309
|
+
}
|
|
1310
|
+
const authorization = readHeader(headers, "authorization");
|
|
1311
|
+
const arcSignature = /^Arc-Signature\s+(.+)$/i.exec(authorization ?? "");
|
|
1312
|
+
if (arcSignature?.[1]) {
|
|
1313
|
+
return arcSignature[1].trim();
|
|
1314
|
+
}
|
|
1315
|
+
const bearer = /^Bearer\s+(.+)$/i.exec(authorization ?? "");
|
|
1316
|
+
return bearer?.[1]?.trim() ?? null;
|
|
1317
|
+
}
|
|
1318
|
+
function readRequestHeaders(request) {
|
|
1319
|
+
if (request instanceof Request) {
|
|
1320
|
+
return request.headers;
|
|
1321
|
+
}
|
|
1322
|
+
if (isPlainObject(request) && request.headers !== undefined) {
|
|
1323
|
+
return request.headers;
|
|
1324
|
+
}
|
|
1325
|
+
return {};
|
|
1326
|
+
}
|
|
1327
|
+
function attachArcAuthority(request, claims) {
|
|
1328
|
+
if (request && typeof request === "object") {
|
|
1329
|
+
request.arcAuthority = claims;
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
function verifyTimestamp(timestamp, options) {
|
|
1333
|
+
const parsed = Date.parse(timestamp);
|
|
1334
|
+
if (!Number.isFinite(parsed)) {
|
|
1335
|
+
throw new ArcRequestVerificationError("stale_timestamp", "Arc timestamp is invalid.");
|
|
1336
|
+
}
|
|
1337
|
+
const now = options.now?.getTime() ?? Date.now();
|
|
1338
|
+
const skewMs = options.maxTimestampSkewMs ?? defaultMaxTimestampSkewMs;
|
|
1339
|
+
if (Math.abs(now - parsed) > skewMs) {
|
|
1340
|
+
throw new ArcRequestVerificationError("stale_timestamp", "Arc timestamp is outside the freshness window.");
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
async function verifyNonce(nonce, timestamp, options) {
|
|
1344
|
+
const skewMs = options.maxTimestampSkewMs ?? defaultMaxTimestampSkewMs;
|
|
1345
|
+
const expiresAt = new Date(Date.parse(timestamp) + skewMs);
|
|
1346
|
+
const store = options.nonceStore ?? (options.unsafeAllowInMemoryNonceStore ? defaultNonceStore : null);
|
|
1347
|
+
if (!store) {
|
|
1348
|
+
throw new ArcRequestVerificationError("nonce_store_required", "Arc request verification requires a nonceStore backed by persistent shared storage.");
|
|
1349
|
+
}
|
|
1350
|
+
const accepted = await store.useNonce(nonce, expiresAt);
|
|
1351
|
+
if (!accepted) {
|
|
1352
|
+
throw new ArcRequestVerificationError("nonce_replayed", "Arc nonce has already been used.");
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
async function normalizeActionRequest(request) {
|
|
1356
|
+
if (isFetchRequest(request)) {
|
|
1357
|
+
return {
|
|
1358
|
+
body: await request.text(),
|
|
1359
|
+
headers: request.headers
|
|
1360
|
+
};
|
|
1361
|
+
}
|
|
1362
|
+
if (isObjectWithHeaders(request)) {
|
|
1363
|
+
const body = "body" in request && request.body !== undefined
|
|
1364
|
+
? request.body
|
|
1365
|
+
: await readRequestStream(request);
|
|
1366
|
+
return {
|
|
1367
|
+
body: normalizeRequestBody(body),
|
|
1368
|
+
headers: normalizeRequestHeaders(request.headers)
|
|
1369
|
+
};
|
|
1370
|
+
}
|
|
1371
|
+
throw new ArcValidationError("invalid_request", "Arc action handler received an unsupported request.");
|
|
1372
|
+
}
|
|
1373
|
+
function normalizeRequestBody(body) {
|
|
1374
|
+
if (typeof body === "string" || body instanceof Uint8Array || isArcExecutionRequestBody(body)) {
|
|
1375
|
+
return body;
|
|
1376
|
+
}
|
|
1377
|
+
if (Buffer.isBuffer(body)) {
|
|
1378
|
+
return new Uint8Array(body);
|
|
1379
|
+
}
|
|
1380
|
+
if (isPlainObject(body)) {
|
|
1381
|
+
return body;
|
|
1382
|
+
}
|
|
1383
|
+
throw new ArcValidationError("invalid_request", "Arc request body is unsupported.");
|
|
1384
|
+
}
|
|
1385
|
+
function normalizeRequestHeaders(headers) {
|
|
1386
|
+
if (headers instanceof Headers) {
|
|
1387
|
+
return headers;
|
|
1388
|
+
}
|
|
1389
|
+
if (!isPlainObject(headers)) {
|
|
1390
|
+
return {};
|
|
1391
|
+
}
|
|
1392
|
+
const normalized = {};
|
|
1393
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
1394
|
+
if (typeof value === "string" || Array.isArray(value)) {
|
|
1395
|
+
normalized[key] = value;
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
return normalized;
|
|
1399
|
+
}
|
|
1400
|
+
async function readRequestStream(request) {
|
|
1401
|
+
if (!isAsyncIterable(request)) {
|
|
1402
|
+
return "";
|
|
1403
|
+
}
|
|
1404
|
+
const chunks = [];
|
|
1405
|
+
for await (const chunk of request) {
|
|
1406
|
+
if (typeof chunk === "string") {
|
|
1407
|
+
chunks.push(Buffer.from(chunk));
|
|
1408
|
+
}
|
|
1409
|
+
else if (chunk instanceof Uint8Array) {
|
|
1410
|
+
chunks.push(chunk);
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
1414
|
+
}
|
|
1415
|
+
function sendActionResponse(request, response, statusCode, body) {
|
|
1416
|
+
if (isExpressResponse(response)) {
|
|
1417
|
+
return response.status(statusCode).json(body);
|
|
1418
|
+
}
|
|
1419
|
+
if (isFastifyReply(response)) {
|
|
1420
|
+
return response.code(statusCode).send(body);
|
|
1421
|
+
}
|
|
1422
|
+
if (isFetchRequest(request) && typeof Response !== "undefined") {
|
|
1423
|
+
return Response.json(body, { status: statusCode });
|
|
1424
|
+
}
|
|
1425
|
+
return body;
|
|
1426
|
+
}
|
|
1427
|
+
function isFetchRequest(value) {
|
|
1428
|
+
return typeof Request !== "undefined" && value instanceof Request;
|
|
1429
|
+
}
|
|
1430
|
+
function isObjectWithHeaders(value) {
|
|
1431
|
+
return Boolean(value && typeof value === "object" && "headers" in value);
|
|
1432
|
+
}
|
|
1433
|
+
function isAsyncIterable(value) {
|
|
1434
|
+
return Boolean(value && typeof value === "object" && Symbol.asyncIterator in value);
|
|
1435
|
+
}
|
|
1436
|
+
function isExpressResponse(value) {
|
|
1437
|
+
return Boolean(value &&
|
|
1438
|
+
typeof value === "object" &&
|
|
1439
|
+
"status" in value &&
|
|
1440
|
+
typeof value.status === "function");
|
|
1441
|
+
}
|
|
1442
|
+
function isFastifyReply(value) {
|
|
1443
|
+
return Boolean(value &&
|
|
1444
|
+
typeof value === "object" &&
|
|
1445
|
+
"code" in value &&
|
|
1446
|
+
typeof value.code === "function");
|
|
1447
|
+
}
|
|
1448
|
+
function isArcExecutionRequestBody(value) {
|
|
1449
|
+
return Boolean(value &&
|
|
1450
|
+
typeof value === "object" &&
|
|
1451
|
+
"invocation_id" in value &&
|
|
1452
|
+
"action_key" in value &&
|
|
1453
|
+
"app_id" in value);
|
|
1454
|
+
}
|
|
1455
|
+
function isJwks(value) {
|
|
1456
|
+
return Boolean(value &&
|
|
1457
|
+
typeof value === "object" &&
|
|
1458
|
+
Array.isArray(value.keys));
|
|
1459
|
+
}
|
|
1460
|
+
function parseJsonResponse(text) {
|
|
1461
|
+
try {
|
|
1462
|
+
return JSON.parse(text);
|
|
1463
|
+
}
|
|
1464
|
+
catch {
|
|
1465
|
+
throw new ArcError("invalid_json", "Arc response was not valid JSON.");
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
function parseSetCookie(headers) {
|
|
1469
|
+
const setCookie = headers.get("set-cookie");
|
|
1470
|
+
if (!setCookie) {
|
|
1471
|
+
return null;
|
|
1472
|
+
}
|
|
1473
|
+
const [cookie] = setCookie.split(";");
|
|
1474
|
+
return cookie?.trim() || null;
|
|
1475
|
+
}
|
|
1476
|
+
function asServerError(value) {
|
|
1477
|
+
if (isPlainObject(value) &&
|
|
1478
|
+
isPlainObject(value.error) &&
|
|
1479
|
+
typeof value.error.code === "string" &&
|
|
1480
|
+
typeof value.error.message === "string") {
|
|
1481
|
+
return {
|
|
1482
|
+
code: value.error.code,
|
|
1483
|
+
message: value.error.message
|
|
1484
|
+
};
|
|
1485
|
+
}
|
|
1486
|
+
return {
|
|
1487
|
+
code: "request_failed",
|
|
1488
|
+
message: "Arc request failed."
|
|
1489
|
+
};
|
|
1490
|
+
}
|
|
1491
|
+
function sha256Hex(value) {
|
|
1492
|
+
return createHash("sha256").update(value, "utf8").digest("hex");
|
|
1493
|
+
}
|
|
1494
|
+
function canonicalJson(value) {
|
|
1495
|
+
return JSON.stringify(sortJson(value));
|
|
1496
|
+
}
|
|
1497
|
+
function sortJson(value) {
|
|
1498
|
+
if (Array.isArray(value)) {
|
|
1499
|
+
return value.map(sortJson);
|
|
1500
|
+
}
|
|
1501
|
+
if (value && typeof value === "object") {
|
|
1502
|
+
return Object.fromEntries(Object.entries(value)
|
|
1503
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
1504
|
+
.map(([key, item]) => [key, sortJson(item)]));
|
|
1505
|
+
}
|
|
1506
|
+
return value;
|
|
1507
|
+
}
|
|
1508
|
+
function isPlainObject(value) {
|
|
1509
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
1510
|
+
}
|
|
1511
|
+
function trimTrailingSlash(value) {
|
|
1512
|
+
return value.replace(/\/+$/u, "");
|
|
1513
|
+
}
|
|
1514
|
+
//# sourceMappingURL=index.js.map
|