@shieldaai/agent-map 0.0.0-stage → 0.1.0-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/oidc.js ADDED
@@ -0,0 +1,311 @@
1
+ import { request as httpsRequest } from 'node:https';
2
+ import { lookup } from 'node:dns';
3
+ import { X509Certificate } from 'node:crypto';
4
+ import { rootCertificates, createSecureContext } from 'node:tls';
5
+ import { immutableJson } from '@shieldaai/agent-guard';
6
+ import * as oidc from 'openid-client';
7
+ import { fields, integer, fail, parseRequest } from './boundary.js';
8
+ import { MapQueryError, mapIdentifier } from './types.js';
9
+ const INVALID = 'MAP_IDENTITY_INVALID', UNAVAILABLE = 'MAP_IDENTITY_UNAVAILABLE';
10
+ function httpsUrl(value, code, query = false) {
11
+ if (typeof value !== 'string' || value.length > 4096 || /[\s\\]/u.test(value))
12
+ fail(code);
13
+ let url;
14
+ try {
15
+ url = new URL(value);
16
+ }
17
+ catch {
18
+ return fail(code);
19
+ }
20
+ if (url.protocol !== 'https:' || url.username || url.password || url.hash || (!query && url.search))
21
+ fail(code);
22
+ return url;
23
+ }
24
+ function opaque(value) {
25
+ if (typeof value !== 'string' || !/^[A-Za-z0-9_-]{43,128}$/.test(value))
26
+ fail(INVALID);
27
+ return value;
28
+ }
29
+ /** Confidential OIDC client: provider tokens never leave this module. */
30
+ export async function createMapOidcClient(options) {
31
+ let copied;
32
+ try {
33
+ copied = immutableJson(options, 4, 128, 512 * 1024);
34
+ }
35
+ catch {
36
+ return fail('MAP_CONFIG_INVALID');
37
+ }
38
+ const config = fields(copied, ['issuer', 'clientId', 'clientSecret', 'redirectUri', 'clientAuthMethod',
39
+ 'endpointOrigins', 'ca', 'requestTimeoutMs', 'maxResponseBytes'], ['issuer', 'clientId', 'clientSecret', 'redirectUri']);
40
+ const issuer = httpsUrl(config.issuer, 'MAP_CONFIG_INVALID');
41
+ if (issuer.pathname.includes('/.well-known/'))
42
+ fail('MAP_CONFIG_INVALID');
43
+ const redirect = httpsUrl(config.redirectUri, 'MAP_CONFIG_INVALID');
44
+ if (redirect.pathname !== '/auth/callback')
45
+ fail('MAP_CONFIG_INVALID');
46
+ const clientId = mapIdentifier(config.clientId, 'MAP_CONFIG_INVALID');
47
+ const secret = mapIdentifier(config.clientSecret, 'MAP_CONFIG_INVALID');
48
+ if (clientId.length > 512 || secret.length > 4096)
49
+ fail('MAP_CONFIG_INVALID');
50
+ const method = config.clientAuthMethod ?? 'client_secret_basic';
51
+ if (method !== 'client_secret_basic' && method !== 'client_secret_post')
52
+ fail('MAP_CONFIG_INVALID');
53
+ const authentication = () => method === 'client_secret_basic' ? oidc.ClientSecretBasic(secret) : oidc.ClientSecretPost(secret);
54
+ const timeout = integer(config.requestTimeoutMs, 5000, 50, 10_000);
55
+ const maxBytes = integer(config.maxResponseBytes, 256 * 1024, 1024, 1024 * 1024);
56
+ const origins = new Set([issuer.origin]);
57
+ if (config.endpointOrigins !== undefined) {
58
+ if (!Array.isArray(config.endpointOrigins) || config.endpointOrigins.length > 8)
59
+ fail('MAP_CONFIG_INVALID');
60
+ for (const entry of config.endpointOrigins) {
61
+ const url = httpsUrl(entry, 'MAP_CONFIG_INVALID');
62
+ if (entry !== url.origin || origins.has(url.origin))
63
+ fail('MAP_CONFIG_INVALID');
64
+ origins.add(url.origin);
65
+ }
66
+ }
67
+ let ca;
68
+ if (config.ca !== undefined) {
69
+ if (typeof config.ca !== 'string' || !config.ca || Buffer.byteLength(config.ca) > 256 * 1024)
70
+ fail('MAP_CONFIG_INVALID');
71
+ const certificates = config.ca.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g);
72
+ if (!certificates?.length || certificates.length > 16 || config.ca.replace(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g, '').trim())
73
+ fail('MAP_CONFIG_INVALID');
74
+ try {
75
+ for (const certificate of certificates)
76
+ new X509Certificate(certificate);
77
+ }
78
+ catch {
79
+ fail('MAP_CONFIG_INVALID');
80
+ }
81
+ ca = [...rootCertificates, config.ca];
82
+ try {
83
+ createSecureContext({ ca, minVersion: 'TLSv1.2' });
84
+ }
85
+ catch {
86
+ fail('MAP_CONFIG_INVALID');
87
+ }
88
+ }
89
+ let requests = 0, pendingLookups = 0;
90
+ function operation(external) {
91
+ const deadline = performance.now() + timeout, active = new Set();
92
+ let closed = false, failure;
93
+ const check = () => { if (closed || external?.aborted || performance.now() >= deadline)
94
+ fail(UNAVAILABLE); };
95
+ const fetch = (input, init) => new Promise((resolve, reject) => {
96
+ let request, settled = false, counted = false;
97
+ let timer;
98
+ const abort = () => stop(UNAVAILABLE);
99
+ const clean = () => {
100
+ if (timer)
101
+ clearTimeout(timer);
102
+ external?.removeEventListener('abort', abort);
103
+ init.signal?.removeEventListener('abort', abort);
104
+ if (request)
105
+ active.delete(request);
106
+ if (counted)
107
+ requests--;
108
+ };
109
+ const stop = (code) => {
110
+ if (settled)
111
+ return;
112
+ settled = true;
113
+ failure = new MapQueryError(code);
114
+ clean();
115
+ request?.destroy();
116
+ reject(failure);
117
+ };
118
+ try {
119
+ check();
120
+ if (init.signal?.aborted)
121
+ fail(UNAVAILABLE);
122
+ const url = httpsUrl(input, INVALID, true);
123
+ if (!origins.has(url.origin))
124
+ fail(INVALID);
125
+ if (!['GET', 'POST'].includes(init.method) || requests >= 8)
126
+ fail(UNAVAILABLE);
127
+ let body;
128
+ if (init.body !== undefined && init.body !== null) {
129
+ if (typeof init.body === 'string' || init.body instanceof URLSearchParams)
130
+ body = init.body.toString();
131
+ else
132
+ fail(INVALID);
133
+ if (init.method !== 'POST' || Buffer.byteLength(body) > 65_536)
134
+ fail(INVALID);
135
+ }
136
+ requests++;
137
+ counted = true;
138
+ const boundedLookup = (hostname, lookupOptions, callback) => {
139
+ if (closed || external?.aborted || performance.now() >= deadline || pendingLookups >= 8) {
140
+ callback(new Error('OIDC_LOOKUP_UNAVAILABLE'), lookupOptions.all ? [] : '');
141
+ return;
142
+ }
143
+ pendingLookups++;
144
+ lookup(hostname, { ...lookupOptions, all: true }, (error, addresses) => {
145
+ pendingLookups--;
146
+ // A late DNS completion must never reconnect or dispatch credentials.
147
+ if (closed || settled || external?.aborted || performance.now() >= deadline) {
148
+ callback(new Error('OIDC_LOOKUP_UNAVAILABLE'), lookupOptions.all ? [] : '');
149
+ return;
150
+ }
151
+ if (lookupOptions.all)
152
+ callback(error, addresses);
153
+ else
154
+ callback(error, addresses?.[0]?.address ?? '', addresses?.[0]?.family);
155
+ });
156
+ };
157
+ request = httpsRequest(url, { method: init.method, headers: { ...init.headers, 'accept-encoding': 'identity', connection: 'close' },
158
+ agent: false, rejectUnauthorized: true, minVersion: 'TLSv1.2', maxHeaderSize: 8192,
159
+ lookup: boundedLookup, ...(ca === undefined ? {} : { ca }) }, response => {
160
+ const status = response.statusCode ?? 0;
161
+ const count = (name) => response.rawHeaders.filter((item, index) => index % 2 === 0 && item.toLowerCase() === name).length;
162
+ const length = response.headers['content-length'];
163
+ if ((status >= 300 && status < 400) || status >= 500 || status < 200 ||
164
+ count('content-length') > 1 || count('content-type') !== 1 || count('transfer-encoding') > 1 ||
165
+ response.headers['content-encoding'] !== undefined || (length !== undefined && response.headers['transfer-encoding'] !== undefined) ||
166
+ !/^application\/(?:json|jwk-set\+json)(?:\s*;\s*charset=utf-8)?$/i.test(response.headers['content-type'] ?? '') ||
167
+ (length !== undefined && (!/^\d+$/.test(length) || !Number.isSafeInteger(Number(length)) || Number(length) > maxBytes))) {
168
+ response.destroy();
169
+ stop(UNAVAILABLE);
170
+ return;
171
+ }
172
+ const chunks = [];
173
+ let size = 0;
174
+ response.on('data', (chunk) => {
175
+ size += chunk.length;
176
+ if (size > maxBytes || performance.now() >= deadline) {
177
+ response.destroy();
178
+ stop(UNAVAILABLE);
179
+ }
180
+ else
181
+ chunks.push(chunk);
182
+ });
183
+ response.once('aborted', () => stop(UNAVAILABLE));
184
+ response.once('error', () => stop(UNAVAILABLE));
185
+ response.once('end', () => {
186
+ if (settled)
187
+ return;
188
+ try {
189
+ check();
190
+ if (!response.complete || (length !== undefined && Number(length) !== size))
191
+ fail(UNAVAILABLE);
192
+ // Reject ambiguous or malformed JSON before the protocol library consumes it.
193
+ const bytes = Buffer.concat(chunks, size);
194
+ parseRequest(bytes, maxBytes);
195
+ const headers = new Headers();
196
+ for (let index = 0; index < response.rawHeaders.length; index += 2)
197
+ headers.append(response.rawHeaders[index], response.rawHeaders[index + 1]);
198
+ const result = new Response(new Uint8Array(bytes), { status, headers });
199
+ settled = true;
200
+ clean();
201
+ resolve(result);
202
+ }
203
+ catch {
204
+ stop(UNAVAILABLE);
205
+ }
206
+ });
207
+ });
208
+ active.add(request);
209
+ request.once('error', () => stop(UNAVAILABLE));
210
+ timer = setTimeout(abort, Math.max(1, deadline - performance.now()));
211
+ timer.unref();
212
+ external?.addEventListener('abort', abort, { once: true });
213
+ init.signal?.addEventListener('abort', abort, { once: true });
214
+ if (external?.aborted || init.signal?.aborted) {
215
+ stop(UNAVAILABLE);
216
+ return;
217
+ }
218
+ request.end(body);
219
+ }
220
+ catch (error) {
221
+ stop(error instanceof MapQueryError ? error.code : UNAVAILABLE);
222
+ }
223
+ });
224
+ return { fetch, check, get failure() { return failure; }, close() { closed = true; for (const request of active)
225
+ request.destroy(); } };
226
+ }
227
+ const clientMetadata = { id_token_signed_response_alg: 'RS256', [oidc.clockTolerance]: 0 };
228
+ const discovery = operation();
229
+ let metadata;
230
+ try {
231
+ const discovered = await oidc.discovery(issuer, clientId, clientMetadata, authentication(), { [oidc.customFetch]: discovery.fetch, timeout: timeout / 1000 });
232
+ discovery.check();
233
+ const remote = discovered.serverMetadata();
234
+ if (remote.issuer !== config.issuer || !Array.isArray(remote.id_token_signing_alg_values_supported) ||
235
+ !remote.id_token_signing_alg_values_supported.includes('RS256') ||
236
+ !Array.isArray(remote.code_challenge_methods_supported) || !remote.code_challenge_methods_supported.includes('S256') ||
237
+ (remote.token_endpoint_auth_methods_supported === undefined ? method !== 'client_secret_basic' :
238
+ !Array.isArray(remote.token_endpoint_auth_methods_supported) || !remote.token_endpoint_auth_methods_supported.includes(method)))
239
+ fail(INVALID);
240
+ for (const name of ['authorization_endpoint', 'token_endpoint', 'jwks_uri']) {
241
+ if (!origins.has(httpsUrl(remote[name], INVALID).origin))
242
+ fail(INVALID);
243
+ }
244
+ metadata = { issuer: remote.issuer, authorization_endpoint: remote.authorization_endpoint,
245
+ token_endpoint: remote.token_endpoint, jwks_uri: remote.jwks_uri, id_token_signing_alg_values_supported: ['RS256'],
246
+ code_challenge_methods_supported: ['S256'], token_endpoint_auth_methods_supported: [method],
247
+ authorization_response_iss_parameter_supported: remote.authorization_response_iss_parameter_supported === true };
248
+ }
249
+ catch (error) {
250
+ throw discovery.failure ?? new MapQueryError(error instanceof MapQueryError ? error.code : INVALID);
251
+ }
252
+ finally {
253
+ discovery.close();
254
+ }
255
+ const configuration = (fetch) => {
256
+ const client = new oidc.Configuration(metadata, clientId, clientMetadata, authentication());
257
+ client[oidc.customFetch] = fetch;
258
+ client.timeout = timeout / 1000;
259
+ oidc.enableNonRepudiationChecks(client);
260
+ return client;
261
+ };
262
+ return Object.freeze({
263
+ async begin() {
264
+ const op = operation();
265
+ try {
266
+ const state = oidc.randomState(), nonce = oidc.randomNonce(), codeVerifier = oidc.randomPKCECodeVerifier();
267
+ const authorizationUrl = oidc.buildAuthorizationUrl(configuration(op.fetch), { response_type: 'code', scope: 'openid',
268
+ redirect_uri: redirect.href, state, nonce, code_challenge_method: 'S256', code_challenge: await oidc.calculatePKCECodeChallenge(codeVerifier) });
269
+ op.check();
270
+ return Object.freeze({ authorizationUrl: authorizationUrl.href, state, nonce, codeVerifier });
271
+ }
272
+ catch {
273
+ throw new MapQueryError(UNAVAILABLE);
274
+ }
275
+ finally {
276
+ op.close();
277
+ }
278
+ },
279
+ async complete(callbackUrl, transaction, signal) {
280
+ const op = operation(signal);
281
+ try {
282
+ op.check();
283
+ if (!(callbackUrl instanceof URL) || callbackUrl.href.length > 8192 || callbackUrl.origin !== redirect.origin ||
284
+ callbackUrl.pathname !== redirect.pathname || callbackUrl.hash || callbackUrl.username || callbackUrl.password)
285
+ fail(INVALID);
286
+ const callback = new URL(callbackUrl.href), names = [...callback.searchParams.keys()];
287
+ if (names.length > 16 || new Set(names).size !== names.length || names.some(name => !name || name.length > 128) ||
288
+ callback.searchParams.get('error') !== null || callback.searchParams.get('response') !== null ||
289
+ !callback.searchParams.get('code') || !callback.searchParams.get('state'))
290
+ fail(INVALID);
291
+ const value = fields(transaction, ['state', 'nonce', 'codeVerifier'], ['state', 'nonce', 'codeVerifier']);
292
+ const state = opaque(value.state), nonce = opaque(value.nonce), verifier = opaque(value.codeVerifier);
293
+ const tokens = await oidc.authorizationCodeGrant(configuration(op.fetch), callback, { expectedState: state, expectedNonce: nonce, pkceCodeVerifier: verifier, idTokenExpected: true });
294
+ op.check();
295
+ const claims = tokens.claims(), now = Date.now();
296
+ if (!claims || claims.iss !== config.issuer || (claims.azp !== undefined && claims.azp !== clientId) ||
297
+ !Number.isSafeInteger(claims.exp) || !Number.isSafeInteger(claims.iat) || claims.iat > Math.floor(now / 1000) ||
298
+ claims.exp <= claims.iat || !Number.isSafeInteger(claims.exp * 1000) || claims.exp * 1000 <= now)
299
+ fail(INVALID);
300
+ const subject = mapIdentifier(claims.sub, INVALID);
301
+ return Object.freeze({ subject, expiresAt: claims.exp * 1000 });
302
+ }
303
+ catch (error) {
304
+ throw op.failure ?? new MapQueryError(error instanceof MapQueryError && error.code === UNAVAILABLE ? UNAVAILABLE : INVALID);
305
+ }
306
+ finally {
307
+ op.close();
308
+ }
309
+ },
310
+ });
311
+ }
@@ -0,0 +1,28 @@
1
+ import { type JsonObject, type MapEdge, type MapNode, type MapPolicyCandidate, type MapPolicyEvidence } from './types.js';
2
+ interface Probe {
3
+ readonly key: number;
4
+ readonly identity: MapNode;
5
+ readonly resource: MapNode;
6
+ readonly accountId: string;
7
+ readonly partition: string;
8
+ readonly principalArn: string;
9
+ readonly bucketArn: string;
10
+ readonly bucketName: string;
11
+ readonly principalLabel: 'AWSRole' | 'AWSUser';
12
+ readonly relationship: MapPolicyCandidate['relationship'];
13
+ readonly collectionId: string;
14
+ readonly proof: Readonly<Record<string, string | null>>;
15
+ }
16
+ export interface PolicyPlan {
17
+ readonly records: readonly MapPolicyEvidence[];
18
+ readonly probes: readonly Probe[];
19
+ }
20
+ /** Pair only the selected task's grants with its deployment's declared execution identity. */
21
+ export declare function policyPlan(companyId: string, tenantId: string, nodes: readonly MapNode[], edges: readonly MapEdge[], maximum: number): PolicyPlan;
22
+ export declare const POLICY_QUERY: string;
23
+ export declare const POLICY_COLUMNS: readonly ['key', 'bindingProof', 'identity', 'resource', 'identityLink', 'resourceLink', 'principal', 'bucket', 'principalAccount', 'bucketAccount', 'principalOwnership', 'bucketOwnership', 'collection', 'principalMember', 'bucketMember', 'candidate'];
24
+ export declare function policyParameters(plan: PolicyPlan): readonly Record<string, unknown>[];
25
+ export declare function safePolicyRow(raw: unknown): JsonObject;
26
+ /** Pure classification: no positive candidate survives a malformed endpoint or stale receipt. */
27
+ export declare function policyEvidence(plan: PolicyPlan, rows: readonly JsonObject[], now: number, maximumAge: number): readonly MapPolicyEvidence[];
28
+ export {};