@caracalai/sdk 0.1.3-rc.1 → 0.1.5-rc.1
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/client.d.ts +22 -14
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +219 -91
- package/dist/client.js.map +1 -1
- package/dist/coordinator.d.ts +9 -5
- package/dist/coordinator.d.ts.map +1 -1
- package/dist/coordinator.js +11 -5
- package/dist/coordinator.js.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/primitives.d.ts +49 -14
- package/dist/primitives.d.ts.map +1 -1
- package/dist/primitives.js +94 -60
- package/dist/primitives.js.map +1 -1
- package/package.json +4 -4
package/dist/client.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { type CaracalContext } from "./context.js";
|
|
2
2
|
import { type HeaderGetter } from "./envelope.js";
|
|
3
3
|
import { type CoordinatorClient } from "./coordinator.js";
|
|
4
|
-
import {
|
|
4
|
+
import { type Grant, type ServiceAgent } from "./primitives.js";
|
|
5
|
+
import { type DelegationConstraints } from "./coordinator.js";
|
|
5
6
|
import type { JsonObject } from "./json.js";
|
|
6
7
|
export interface ResourceBinding {
|
|
7
8
|
resourceId: string;
|
|
@@ -16,17 +17,26 @@ export interface CaracalConfig {
|
|
|
16
17
|
tokenSource?: TokenSource;
|
|
17
18
|
gatewayUrl?: string;
|
|
18
19
|
resources?: ResourceBinding[];
|
|
19
|
-
defaultKind?: AgentKind;
|
|
20
20
|
defaultTtlSeconds?: number;
|
|
21
21
|
}
|
|
22
22
|
export interface SpawnOptions {
|
|
23
|
-
|
|
23
|
+
grant?: Grant;
|
|
24
24
|
ttlSeconds?: number;
|
|
25
25
|
subjectSessionId?: string;
|
|
26
26
|
parentId?: string;
|
|
27
27
|
metadata?: JsonObject;
|
|
28
|
+
labels?: string[];
|
|
28
29
|
traceId?: string;
|
|
29
30
|
}
|
|
31
|
+
export interface ServiceOptions {
|
|
32
|
+
ttlSeconds?: number;
|
|
33
|
+
subjectSessionId?: string;
|
|
34
|
+
parentId?: string;
|
|
35
|
+
metadata?: JsonObject;
|
|
36
|
+
labels?: string[];
|
|
37
|
+
traceId?: string;
|
|
38
|
+
heartbeatIntervalMs?: number;
|
|
39
|
+
}
|
|
30
40
|
export interface DelegateOptions {
|
|
31
41
|
to: string;
|
|
32
42
|
toApplicationId: string;
|
|
@@ -35,16 +45,6 @@ export interface DelegateOptions {
|
|
|
35
45
|
constraints?: DelegationConstraints;
|
|
36
46
|
ttlSeconds?: number;
|
|
37
47
|
}
|
|
38
|
-
export interface DelegateToSpawnOptions {
|
|
39
|
-
resourceId?: string;
|
|
40
|
-
scopes: string[];
|
|
41
|
-
constraints?: DelegationConstraints;
|
|
42
|
-
delegationTtlSeconds?: number;
|
|
43
|
-
kind?: AgentKind;
|
|
44
|
-
ttlSeconds?: number;
|
|
45
|
-
metadata?: JsonObject;
|
|
46
|
-
traceId?: string;
|
|
47
|
-
}
|
|
48
48
|
export type LifecycleHook = (ctx: CaracalContext) => void | Promise<void>;
|
|
49
49
|
export interface RootOptions {
|
|
50
50
|
allowRoot?: boolean;
|
|
@@ -58,6 +58,7 @@ export interface ClientSecretOptions {
|
|
|
58
58
|
resources: Array<string | ResourceBinding>;
|
|
59
59
|
gatewayUrl?: string;
|
|
60
60
|
scope?: string;
|
|
61
|
+
fetchImpl?: typeof fetch;
|
|
61
62
|
}
|
|
62
63
|
export interface ConnectOptions {
|
|
63
64
|
env?: NodeJS.ProcessEnv;
|
|
@@ -82,8 +83,8 @@ export declare class Caracal {
|
|
|
82
83
|
static fromConfig(path?: string, env?: NodeJS.ProcessEnv): Caracal;
|
|
83
84
|
close(): Promise<void>;
|
|
84
85
|
spawn<T>(fn: () => Promise<T>, opts?: SpawnOptions): Promise<T>;
|
|
86
|
+
service(opts?: ServiceOptions): Promise<ServiceAgent>;
|
|
85
87
|
delegate<T>(opts: DelegateOptions, fn: () => Promise<T>): Promise<T>;
|
|
86
|
-
delegateToSpawn<T>(opts: DelegateToSpawnOptions, fn: () => Promise<T>): Promise<T>;
|
|
87
88
|
bind<T>(ctx: CaracalContext, fn: () => Promise<T>): Promise<T>;
|
|
88
89
|
onAgentStart(cb: LifecycleHook): void;
|
|
89
90
|
onAgentEnd(cb: LifecycleHook): void;
|
|
@@ -100,6 +101,13 @@ export declare class Caracal {
|
|
|
100
101
|
*/
|
|
101
102
|
transport(opts?: RootOptions): typeof fetch;
|
|
102
103
|
gatewayRequest(resourceId: string, path?: string): GatewayRequest;
|
|
104
|
+
/**
|
|
105
|
+
* One-call happy path: sends `init` to `path` on the given resource through the
|
|
106
|
+
* Gateway with Caracal context and authority injected. Equivalent to building a
|
|
107
|
+
* `gatewayRequest` and calling it with `transport`. The resource header always
|
|
108
|
+
* wins over any caller-supplied `X-Caracal-Resource`.
|
|
109
|
+
*/
|
|
110
|
+
fetch(resourceId: string, path?: string, init?: RequestInit, opts?: RootOptions): Promise<Response>;
|
|
103
111
|
private routeThroughGateway;
|
|
104
112
|
/**
|
|
105
113
|
* Binds Caracal context after a verifier boundary. This does not verify JWT
|
package/dist/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAOA,OAAO,EAA2C,KAAK,cAAc,EAAE,MAAM,cAAc,CAAC;AAK5F,OAAO,EAIL,KAAK,YAAY,EAClB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,KAAK,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|
package/dist/client.js
CHANGED
|
@@ -6,13 +6,15 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { bind, fromEnvelope, toEnvelope, current } from "./context.js";
|
|
8
8
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
9
|
-
import { homedir } from "node:os";
|
|
9
|
+
import { homedir, platform } from "node:os";
|
|
10
10
|
import { join } from "node:path";
|
|
11
11
|
import { parse } from "smol-toml";
|
|
12
12
|
import { decodeEnvelope, toHeaders, } from "./envelope.js";
|
|
13
|
-
import { spawn as spawnPrimitive,
|
|
14
|
-
import { AgentKind } from "./coordinator.js";
|
|
13
|
+
import { spawn as spawnPrimitive, spawnService as spawnServicePrimitive, delegate as delegatePrimitive, } from "./primitives.js";
|
|
15
14
|
import { OAuthClient } from "@caracalai/oauth";
|
|
15
|
+
const DEFAULT_STS_URL = "http://localhost:8080";
|
|
16
|
+
const DEFAULT_COORDINATOR_URL = "http://localhost:4000";
|
|
17
|
+
const DEFAULT_GATEWAY_URL = "http://localhost:8081";
|
|
16
18
|
export class Caracal {
|
|
17
19
|
config;
|
|
18
20
|
agentStartHooks = [];
|
|
@@ -54,38 +56,35 @@ export class Caracal {
|
|
|
54
56
|
return Caracal.fromEnv(env);
|
|
55
57
|
}
|
|
56
58
|
static fromEnv(env = process.env) {
|
|
57
|
-
const url = env
|
|
59
|
+
const url = serviceUrl(env, "CARACAL_COORDINATOR_URL", DEFAULT_COORDINATOR_URL);
|
|
58
60
|
const zoneId = env.CARACAL_ZONE_ID;
|
|
59
61
|
const applicationId = env.CARACAL_APPLICATION_ID;
|
|
60
62
|
const subjectToken = env.CARACAL_SUBJECT_TOKEN;
|
|
61
|
-
const
|
|
62
|
-
const
|
|
63
|
-
const gatewayUrl = env.CARACAL_GATEWAY_URL;
|
|
63
|
+
const stsUrl = stsUrlFromEnv(env);
|
|
64
|
+
const gatewayUrl = serviceUrl(env, "CARACAL_GATEWAY_URL", DEFAULT_GATEWAY_URL);
|
|
64
65
|
const missing = [
|
|
65
|
-
["CARACAL_COORDINATOR_URL", url],
|
|
66
66
|
["CARACAL_ZONE_ID", zoneId],
|
|
67
67
|
["CARACAL_APPLICATION_ID", applicationId],
|
|
68
68
|
].filter(([, v]) => !v).map(([k]) => k);
|
|
69
69
|
if (missing.length) {
|
|
70
70
|
throw new Error(`Caracal.fromEnv: missing ${missing.join(", ")}`);
|
|
71
71
|
}
|
|
72
|
-
const
|
|
72
|
+
const clientSecret = clientSecretFromEnv(env, zoneId, applicationId);
|
|
73
|
+
const profileResources = resourcesFromEnv(env, zoneId, applicationId);
|
|
73
74
|
const resources = profileResources.bindings;
|
|
74
75
|
if (clientSecret) {
|
|
75
|
-
if (!stsUrl)
|
|
76
|
-
throw new Error("Caracal.fromEnv: CARACAL_APP_CLIENT_SECRET requires CARACAL_STS_URL");
|
|
77
76
|
return Caracal.fromClientSecret({
|
|
78
77
|
coordinatorUrl: url,
|
|
79
78
|
stsUrl,
|
|
80
79
|
zoneId: zoneId,
|
|
81
80
|
applicationId: applicationId,
|
|
82
81
|
clientSecret,
|
|
83
|
-
resources: resourceIdsFromEnv(env.CARACAL_APP_RESOURCES, profileResources.resources),
|
|
82
|
+
resources: resourceIdsFromEnv(env.CARACAL_APP_RESOURCES, profileResources.credentialResources ?? [], profileResources.resources),
|
|
84
83
|
gatewayUrl,
|
|
85
84
|
});
|
|
86
85
|
}
|
|
87
86
|
if (!subjectToken) {
|
|
88
|
-
throw new Error("Caracal.fromEnv: provide CARACAL_APP_CLIENT_SECRET
|
|
87
|
+
throw new Error("Caracal.fromEnv: provide CARACAL_APP_CLIENT_SECRET or CARACAL_SUBJECT_TOKEN");
|
|
89
88
|
}
|
|
90
89
|
validateSubjectToken(subjectToken);
|
|
91
90
|
return new Caracal({
|
|
@@ -106,7 +105,7 @@ export class Caracal {
|
|
|
106
105
|
coordinator: { baseUrl: opts.coordinatorUrl },
|
|
107
106
|
zoneId: opts.zoneId,
|
|
108
107
|
applicationId: opts.applicationId,
|
|
109
|
-
tokenSource: createClientSecretTokenSource(opts.stsUrl, opts.zoneId, opts.applicationId, opts.clientSecret, resourceIds, opts.scope),
|
|
108
|
+
tokenSource: createClientSecretTokenSource(opts.stsUrl, opts.zoneId, opts.applicationId, opts.clientSecret, resourceIds, opts.scope, opts.fetchImpl),
|
|
110
109
|
gatewayUrl: opts.gatewayUrl,
|
|
111
110
|
resources: bindings.length ? bindings : undefined,
|
|
112
111
|
});
|
|
@@ -120,21 +119,24 @@ export class Caracal {
|
|
|
120
119
|
throw new Error("Caracal.fromConfig: profile must be a TOML table");
|
|
121
120
|
const zoneId = requiredString(value, "zone_id", path);
|
|
122
121
|
const applicationId = requiredString(value, "application_id", path);
|
|
123
|
-
const stsUrl = stringValue(value, "sts_url")
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const resources = resourcesFromProfile(value, path);
|
|
122
|
+
const stsUrl = stringValue(value, "sts_url")
|
|
123
|
+
?? stringValue(value, "zone_url")
|
|
124
|
+
?? env.CARACAL_STS_URL
|
|
125
|
+
?? env.CARACAL_ZONE_URL
|
|
126
|
+
?? serviceUrl(env, "CARACAL_STS_URL", DEFAULT_STS_URL);
|
|
127
|
+
const coordinatorUrl = stringValue(value, "coordinator_url") ?? serviceUrl(env, "CARACAL_COORDINATOR_URL", DEFAULT_COORDINATOR_URL);
|
|
128
|
+
const resources = resourcesFromProfile(value, path, env, zoneId, applicationId);
|
|
129
|
+
if (!resources.resources.length) {
|
|
130
|
+
throw new Error(`Caracal.fromConfig: ${path} requires at least one resource via credentials, CARACAL_RESOURCES, or CARACAL_RESOURCES_FILE`);
|
|
131
|
+
}
|
|
130
132
|
return Caracal.fromClientSecret({
|
|
131
133
|
coordinatorUrl,
|
|
132
134
|
stsUrl,
|
|
133
135
|
zoneId,
|
|
134
136
|
applicationId,
|
|
135
|
-
clientSecret: clientSecretFromProfile(value, path),
|
|
137
|
+
clientSecret: clientSecretFromProfile(value, path, env, zoneId, applicationId),
|
|
136
138
|
resources: resources.resources,
|
|
137
|
-
gatewayUrl: stringValue(value, "gateway_url") ?? env
|
|
139
|
+
gatewayUrl: stringValue(value, "gateway_url") ?? serviceUrl(env, "CARACAL_GATEWAY_URL", DEFAULT_GATEWAY_URL),
|
|
138
140
|
});
|
|
139
141
|
}
|
|
140
142
|
async close() {
|
|
@@ -145,17 +147,34 @@ export class Caracal {
|
|
|
145
147
|
zoneId: this.config.zoneId,
|
|
146
148
|
applicationId: this.config.applicationId,
|
|
147
149
|
subjectToken: await this.rootToken(),
|
|
148
|
-
|
|
150
|
+
grant: opts.grant,
|
|
149
151
|
ttlSeconds: opts.ttlSeconds ?? this.config.defaultTtlSeconds,
|
|
150
152
|
subjectSessionId: opts.subjectSessionId,
|
|
151
153
|
parentId: opts.parentId,
|
|
152
154
|
metadata: opts.metadata,
|
|
155
|
+
labels: opts.labels,
|
|
153
156
|
traceId: opts.traceId,
|
|
154
157
|
onAgentStart: this.agentStartHooks.length ? (c) => this.fire(this.agentStartHooks, c) : undefined,
|
|
155
158
|
onAgentEnd: this.agentEndHooks.length ? (c) => this.fire(this.agentEndHooks, c) : undefined,
|
|
156
159
|
};
|
|
157
160
|
return await spawnPrimitive(input, fn);
|
|
158
161
|
}
|
|
162
|
+
async service(opts = {}) {
|
|
163
|
+
return await spawnServicePrimitive({
|
|
164
|
+
coordinator: this.config.coordinator,
|
|
165
|
+
zoneId: this.config.zoneId,
|
|
166
|
+
applicationId: this.config.applicationId,
|
|
167
|
+
subjectToken: await this.rootToken(),
|
|
168
|
+
ttlSeconds: opts.ttlSeconds ?? this.config.defaultTtlSeconds,
|
|
169
|
+
subjectSessionId: opts.subjectSessionId,
|
|
170
|
+
parentId: opts.parentId,
|
|
171
|
+
metadata: opts.metadata,
|
|
172
|
+
labels: opts.labels,
|
|
173
|
+
traceId: opts.traceId,
|
|
174
|
+
heartbeatIntervalMs: opts.heartbeatIntervalMs,
|
|
175
|
+
onAgentStart: this.agentStartHooks.length ? (c) => this.fire(this.agentStartHooks, c) : undefined,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
159
178
|
delegate(opts, fn) {
|
|
160
179
|
const input = {
|
|
161
180
|
coordinator: this.config.coordinator,
|
|
@@ -168,25 +187,6 @@ export class Caracal {
|
|
|
168
187
|
};
|
|
169
188
|
return delegatePrimitive(input, fn);
|
|
170
189
|
}
|
|
171
|
-
async delegateToSpawn(opts, fn) {
|
|
172
|
-
const input = {
|
|
173
|
-
coordinator: this.config.coordinator,
|
|
174
|
-
zoneId: this.config.zoneId,
|
|
175
|
-
applicationId: this.config.applicationId,
|
|
176
|
-
subjectToken: await this.rootToken(),
|
|
177
|
-
resourceId: opts.resourceId,
|
|
178
|
-
scopes: opts.scopes,
|
|
179
|
-
constraints: opts.constraints,
|
|
180
|
-
delegationTtlSeconds: opts.delegationTtlSeconds,
|
|
181
|
-
kind: opts.kind ?? this.config.defaultKind ?? AgentKind.Instance,
|
|
182
|
-
ttlSeconds: opts.ttlSeconds ?? this.config.defaultTtlSeconds,
|
|
183
|
-
metadata: opts.metadata,
|
|
184
|
-
traceId: opts.traceId,
|
|
185
|
-
onAgentStart: this.agentStartHooks.length ? (c) => this.fire(this.agentStartHooks, c) : undefined,
|
|
186
|
-
onAgentEnd: this.agentEndHooks.length ? (c) => this.fire(this.agentEndHooks, c) : undefined,
|
|
187
|
-
};
|
|
188
|
-
return await delegateToSpawnPrimitive(input, fn);
|
|
189
|
-
}
|
|
190
190
|
bind(ctx, fn) {
|
|
191
191
|
return bind(ctx, fn);
|
|
192
192
|
}
|
|
@@ -293,6 +293,19 @@ export class Caracal {
|
|
|
293
293
|
headers: { "X-Caracal-Resource": resourceId },
|
|
294
294
|
};
|
|
295
295
|
}
|
|
296
|
+
/**
|
|
297
|
+
* One-call happy path: sends `init` to `path` on the given resource through the
|
|
298
|
+
* Gateway with Caracal context and authority injected. Equivalent to building a
|
|
299
|
+
* `gatewayRequest` and calling it with `transport`. The resource header always
|
|
300
|
+
* wins over any caller-supplied `X-Caracal-Resource`.
|
|
301
|
+
*/
|
|
302
|
+
fetch(resourceId, path = "/", init = {}, opts = {}) {
|
|
303
|
+
const request = this.gatewayRequest(resourceId, path);
|
|
304
|
+
const headers = new Headers(init.headers ?? {});
|
|
305
|
+
for (const [key, value] of Object.entries(request.headers))
|
|
306
|
+
headers.set(key, value);
|
|
307
|
+
return this.transport(opts)(request.url, { ...init, headers });
|
|
308
|
+
}
|
|
296
309
|
routeThroughGateway(input, explicitResource) {
|
|
297
310
|
const gw = this.config.gatewayUrl;
|
|
298
311
|
if (!gw)
|
|
@@ -354,11 +367,54 @@ export class Caracal {
|
|
|
354
367
|
throw new Error("Caracal client has no subject token source");
|
|
355
368
|
}
|
|
356
369
|
}
|
|
370
|
+
function serviceUrl(env, key, fallback) {
|
|
371
|
+
const value = env[key];
|
|
372
|
+
if (value)
|
|
373
|
+
return value;
|
|
374
|
+
if (env.NODE_ENV === "production")
|
|
375
|
+
throw new Error(`Caracal SDK: ${key} is required when NODE_ENV=production`);
|
|
376
|
+
return fallback;
|
|
377
|
+
}
|
|
378
|
+
function stsUrlFromEnv(env) {
|
|
379
|
+
return env.CARACAL_STS_URL ?? env.CARACAL_ZONE_URL ?? serviceUrl(env, "CARACAL_STS_URL", DEFAULT_STS_URL);
|
|
380
|
+
}
|
|
357
381
|
function defaultProfilePath(env = process.env) {
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
382
|
+
return join(defaultConfigDir(env), "caracal.toml");
|
|
383
|
+
}
|
|
384
|
+
function defaultConfigDir(env = process.env) {
|
|
385
|
+
if (env.CARACAL_CONFIG_HOME)
|
|
386
|
+
return env.CARACAL_CONFIG_HOME;
|
|
387
|
+
if (env.XDG_CONFIG_HOME)
|
|
388
|
+
return join(env.XDG_CONFIG_HOME, "caracal");
|
|
389
|
+
if (platform() === "win32")
|
|
390
|
+
return join(env.APPDATA || env.LOCALAPPDATA || join(homedir(), "AppData", "Roaming"), "Caracal");
|
|
391
|
+
if (platform() === "darwin")
|
|
392
|
+
return join(homedir(), "Library", "Application Support", "Caracal");
|
|
393
|
+
return join(homedir(), ".config", "caracal");
|
|
394
|
+
}
|
|
395
|
+
function defaultCredentialDir(env, zoneId, applicationId) {
|
|
396
|
+
return join(defaultConfigDir(env), "runtime", safePathSegment(zoneId), safePathSegment(applicationId));
|
|
397
|
+
}
|
|
398
|
+
function defaultClientSecretPath(env, zoneId, applicationId) {
|
|
399
|
+
return join(defaultCredentialDir(env, zoneId, applicationId), "client-secret");
|
|
400
|
+
}
|
|
401
|
+
function defaultRunCredentialsPath(env, zoneId, applicationId) {
|
|
402
|
+
return join(defaultCredentialDir(env, zoneId, applicationId), "credentials.json");
|
|
403
|
+
}
|
|
404
|
+
function safePathSegment(value) {
|
|
405
|
+
const segment = value.trim().replace(/[^A-Za-z0-9._-]+/g, "_");
|
|
406
|
+
let start = 0;
|
|
407
|
+
let end = segment.length;
|
|
408
|
+
while (start < end && segment[start] === "_")
|
|
409
|
+
start += 1;
|
|
410
|
+
while (end > start && segment[end - 1] === "_")
|
|
411
|
+
end -= 1;
|
|
412
|
+
return segment.slice(start, end) || "default";
|
|
413
|
+
}
|
|
414
|
+
function existingLocalFile(path, env) {
|
|
415
|
+
if (env.NODE_ENV === "production")
|
|
416
|
+
return undefined;
|
|
417
|
+
return existsSync(path) ? path : undefined;
|
|
362
418
|
}
|
|
363
419
|
function resolveProfilePath(env) {
|
|
364
420
|
if (env.CARACAL_CONFIG) {
|
|
@@ -400,23 +456,27 @@ function requiredString(record, key, source) {
|
|
|
400
456
|
throw new Error(`${source}: ${key} is required`);
|
|
401
457
|
return value;
|
|
402
458
|
}
|
|
403
|
-
function clientSecretFromProfile(record, source) {
|
|
459
|
+
function clientSecretFromProfile(record, source, env, zoneId, applicationId) {
|
|
404
460
|
const inline = stringValue(record, "app_client_secret");
|
|
405
461
|
const file = stringValue(record, "app_client_secret_file");
|
|
406
462
|
if (inline && file)
|
|
407
463
|
throw new Error(`${source}: set only one of app_client_secret or app_client_secret_file`);
|
|
408
464
|
if (inline)
|
|
409
465
|
return inline;
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
466
|
+
const localFile = file ?? existingLocalFile(defaultClientSecretPath(env, zoneId, applicationId), env);
|
|
467
|
+
if (!localFile)
|
|
468
|
+
throw new Error(`${source}: client secret is required; local dev/stable auto-detects ${defaultClientSecretPath(env, zoneId, applicationId)} when it exists`);
|
|
469
|
+
return readSecretFile(localFile);
|
|
413
470
|
}
|
|
414
|
-
function clientSecretFromEnv(env) {
|
|
471
|
+
function clientSecretFromEnv(env, zoneId, applicationId) {
|
|
415
472
|
if (env.CARACAL_APP_CLIENT_SECRET && env.CARACAL_APP_CLIENT_SECRET_FILE) {
|
|
416
473
|
throw new Error("Caracal.fromEnv: set only one of CARACAL_APP_CLIENT_SECRET or CARACAL_APP_CLIENT_SECRET_FILE");
|
|
417
474
|
}
|
|
418
475
|
if (env.CARACAL_APP_CLIENT_SECRET_FILE)
|
|
419
476
|
return readSecretFile(env.CARACAL_APP_CLIENT_SECRET_FILE);
|
|
477
|
+
const localFile = existingLocalFile(defaultClientSecretPath(env, zoneId, applicationId), env);
|
|
478
|
+
if (localFile)
|
|
479
|
+
return readSecretFile(localFile);
|
|
420
480
|
return env.CARACAL_APP_CLIENT_SECRET;
|
|
421
481
|
}
|
|
422
482
|
function readSecretFile(path) {
|
|
@@ -428,64 +488,123 @@ function readSecretFile(path) {
|
|
|
428
488
|
throw new Error(`Caracal profile secret file is empty: ${path}`);
|
|
429
489
|
return secret;
|
|
430
490
|
}
|
|
431
|
-
function resourcesFromProfile(record, source) {
|
|
491
|
+
function resourcesFromProfile(record, source, env, zoneId, applicationId) {
|
|
432
492
|
const credentials = [
|
|
433
493
|
...credentialEntries(record.credentials, `${source}.credentials`),
|
|
434
494
|
...credentialEntries(record.optional_credentials, `${source}.optional_credentials`),
|
|
495
|
+
...credentialManifestFromEnv(env, zoneId, applicationId),
|
|
435
496
|
];
|
|
436
497
|
const resources = resourcesFromCredentials(credentials);
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
const credentials = credentialManifestFromEnv(env);
|
|
443
|
-
const envBindings = [
|
|
444
|
-
...resourceBindingsFromFile(env.CARACAL_RESOURCES_FILE),
|
|
445
|
-
...(parseResourceBindings(env.CARACAL_RESOURCES) ?? []),
|
|
446
|
-
];
|
|
498
|
+
const resolved = resolveProfileResources(resources.resources, resources.bindings ?? [], env);
|
|
499
|
+
return { ...resolved, credentialResources: resources.resources };
|
|
500
|
+
}
|
|
501
|
+
function resourcesFromEnv(env, zoneId, applicationId) {
|
|
502
|
+
const credentials = credentialManifestFromEnv(env, zoneId, applicationId);
|
|
447
503
|
const resources = resourcesFromCredentials(credentials);
|
|
504
|
+
const resolved = resolveProfileResources(resources.resources, resources.bindings ?? [], env);
|
|
505
|
+
return { ...resolved, credentialResources: resources.resources };
|
|
506
|
+
}
|
|
507
|
+
function resolveProfileResources(resources, credentialBindings, env) {
|
|
508
|
+
const envBindings = parseResourceBindings(env.CARACAL_RESOURCES) ?? [];
|
|
509
|
+
const bindings = sortBindingsLongestFirst(mergeResourceBindings(credentialBindings, resourceBindingsFromFile(env.CARACAL_RESOURCES_FILE), envBindings));
|
|
448
510
|
const byResource = new Map();
|
|
449
|
-
for (const item of resources
|
|
511
|
+
for (const item of resources)
|
|
450
512
|
byResource.set(typeof item === "string" ? item : item.resourceId, item);
|
|
451
|
-
for (const binding of
|
|
513
|
+
for (const binding of bindings)
|
|
452
514
|
byResource.set(binding.resourceId, binding);
|
|
453
515
|
const values = [...byResource.values()];
|
|
454
|
-
return { resources: values, bindings
|
|
516
|
+
return { resources: values, bindings };
|
|
455
517
|
}
|
|
456
518
|
function resourceBindingsFromFile(path) {
|
|
457
519
|
if (!path)
|
|
458
520
|
return [];
|
|
459
521
|
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
522
|
+
const errors = [];
|
|
460
523
|
if (Array.isArray(parsed)) {
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
524
|
+
const out = [];
|
|
525
|
+
for (const [index, entry] of parsed.entries()) {
|
|
526
|
+
if (!isRecord(entry)) {
|
|
527
|
+
errors.push(`[${index}]: entry must be an object`);
|
|
528
|
+
continue;
|
|
529
|
+
}
|
|
530
|
+
const keys = Object.keys(entry);
|
|
531
|
+
if (keys.length !== 2 || !keys.includes("resource_id") || !keys.includes("upstream_prefix")) {
|
|
532
|
+
errors.push(`[${index}]: expected exactly resource_id and upstream_prefix`);
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
const resourceId = entry.resource_id;
|
|
536
|
+
const upstreamPrefix = entry.upstream_prefix;
|
|
537
|
+
if (typeof resourceId !== "string" || !resourceId) {
|
|
538
|
+
errors.push(`[${index}]: resource_id must be a non-empty string`);
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
if (typeof upstreamPrefix !== "string" || !upstreamPrefix) {
|
|
542
|
+
errors.push(`[${index}]: upstream_prefix must be a non-empty string`);
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
if (!isAbsoluteUrl(upstreamPrefix)) {
|
|
546
|
+
errors.push(`[${index}]: upstream_prefix must be an absolute URL`);
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
out.push({ resourceId, upstreamPrefix });
|
|
550
|
+
}
|
|
551
|
+
if (errors.length)
|
|
552
|
+
throw new Error(`invalid CARACAL_RESOURCES_FILE:\n- ${errors.join("\n- ")}`);
|
|
553
|
+
return out;
|
|
470
554
|
}
|
|
471
555
|
if (isRecord(parsed)) {
|
|
472
|
-
|
|
556
|
+
const out = [];
|
|
557
|
+
for (const [resourceId, upstreamPrefix] of Object.entries(parsed)) {
|
|
558
|
+
if (!resourceId) {
|
|
559
|
+
errors.push("key must be a non-empty string");
|
|
560
|
+
continue;
|
|
561
|
+
}
|
|
473
562
|
if (typeof upstreamPrefix !== "string" || !upstreamPrefix) {
|
|
474
|
-
|
|
563
|
+
errors.push(`entry ${JSON.stringify(resourceId)} upstream_prefix must be a non-empty string`);
|
|
564
|
+
continue;
|
|
475
565
|
}
|
|
476
|
-
|
|
477
|
-
|
|
566
|
+
if (!isAbsoluteUrl(upstreamPrefix)) {
|
|
567
|
+
errors.push(`entry ${JSON.stringify(resourceId)} upstream_prefix must be an absolute URL`);
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
570
|
+
out.push({ resourceId, upstreamPrefix });
|
|
571
|
+
}
|
|
572
|
+
if (errors.length)
|
|
573
|
+
throw new Error(`invalid CARACAL_RESOURCES_FILE:\n- ${errors.join("\n- ")}`);
|
|
574
|
+
return out;
|
|
478
575
|
}
|
|
479
576
|
throw new Error("CARACAL_RESOURCES_FILE must contain an object or array");
|
|
480
577
|
}
|
|
481
|
-
function
|
|
578
|
+
function mergeResourceBindings(...sources) {
|
|
579
|
+
const order = [];
|
|
580
|
+
const byResource = new Map();
|
|
581
|
+
for (const source of sources) {
|
|
582
|
+
for (const binding of source) {
|
|
583
|
+
if (!byResource.has(binding.resourceId))
|
|
584
|
+
order.push(binding.resourceId);
|
|
585
|
+
byResource.set(binding.resourceId, binding);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
return order.map((resourceId) => byResource.get(resourceId));
|
|
589
|
+
}
|
|
590
|
+
function isAbsoluteUrl(value) {
|
|
591
|
+
try {
|
|
592
|
+
const parsed = new URL(value);
|
|
593
|
+
return Boolean(parsed.protocol && parsed.host);
|
|
594
|
+
}
|
|
595
|
+
catch {
|
|
596
|
+
return false;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
function credentialManifestFromEnv(env, zoneId, applicationId) {
|
|
482
600
|
const file = env.CARACAL_RUN_CREDENTIALS_FILE;
|
|
483
601
|
const inline = env.CARACAL_RUN_CREDENTIALS;
|
|
484
602
|
if (file && inline)
|
|
485
603
|
throw new Error("Caracal.fromEnv: set only one of CARACAL_RUN_CREDENTIALS or CARACAL_RUN_CREDENTIALS_FILE");
|
|
486
|
-
|
|
604
|
+
const localFile = !file && !inline ? existingLocalFile(defaultRunCredentialsPath(env, zoneId, applicationId), env) : undefined;
|
|
605
|
+
if (!file && !inline && !localFile)
|
|
487
606
|
return [];
|
|
488
|
-
const raw = file ? readSecretFile(file) : inline;
|
|
607
|
+
const raw = file || localFile ? readSecretFile(file ?? localFile) : inline;
|
|
489
608
|
const parsed = JSON.parse(raw);
|
|
490
609
|
const manifest = Array.isArray(parsed) ? { credentials: parsed } : parsed;
|
|
491
610
|
if (!isRecord(manifest))
|
|
@@ -581,10 +700,7 @@ function parseResourceBindings(raw) {
|
|
|
581
700
|
errors.push(`entry ${index + 1} must contain non-empty resourceId and upstreamPrefix`);
|
|
582
701
|
continue;
|
|
583
702
|
}
|
|
584
|
-
|
|
585
|
-
new URL(upstreamPrefix);
|
|
586
|
-
}
|
|
587
|
-
catch {
|
|
703
|
+
if (!isAbsoluteUrl(upstreamPrefix)) {
|
|
588
704
|
errors.push(`entry ${index + 1} upstreamPrefix must be an absolute URL`);
|
|
589
705
|
continue;
|
|
590
706
|
}
|
|
@@ -595,16 +711,28 @@ function parseResourceBindings(raw) {
|
|
|
595
711
|
}
|
|
596
712
|
return out.length ? sortBindingsLongestFirst(out) : undefined;
|
|
597
713
|
}
|
|
598
|
-
function
|
|
714
|
+
function compactResourceValues(values) {
|
|
715
|
+
const seen = new Set();
|
|
716
|
+
const out = [];
|
|
717
|
+
for (const value of values) {
|
|
718
|
+
const resourceId = typeof value === "string" ? value : value.resourceId;
|
|
719
|
+
if (!resourceId || seen.has(resourceId))
|
|
720
|
+
continue;
|
|
721
|
+
seen.add(resourceId);
|
|
722
|
+
out.push(value);
|
|
723
|
+
}
|
|
724
|
+
return out;
|
|
725
|
+
}
|
|
726
|
+
function resourceIdsFromEnv(raw, first, resources) {
|
|
599
727
|
const explicit = raw?.split(",").map((value) => value.trim()).filter(Boolean);
|
|
600
728
|
if (explicit?.length)
|
|
601
|
-
return explicit;
|
|
729
|
+
return compactResourceValues([...first, ...explicit]);
|
|
602
730
|
if (resources?.length)
|
|
603
731
|
return resources;
|
|
604
|
-
throw new Error("Caracal.fromEnv: client-secret mode requires resources via CARACAL_APP_RESOURCES, CARACAL_RUN_CREDENTIALS, or
|
|
732
|
+
throw new Error("Caracal.fromEnv: client-secret mode requires resources via CARACAL_APP_RESOURCES, CARACAL_RUN_CREDENTIALS, CARACAL_RESOURCES, or CARACAL_RESOURCES_FILE");
|
|
605
733
|
}
|
|
606
|
-
function createClientSecretTokenSource(stsUrl, zoneId, applicationId, clientSecret, resources, scope = "agent:lifecycle") {
|
|
607
|
-
const client = new OAuthClient(stsUrl, zoneId, applicationId);
|
|
734
|
+
function createClientSecretTokenSource(stsUrl, zoneId, applicationId, clientSecret, resources, scope = "agent:lifecycle", fetchImpl) {
|
|
735
|
+
const client = new OAuthClient(stsUrl, zoneId, applicationId, undefined, fetchImpl);
|
|
608
736
|
return async () => {
|
|
609
737
|
const token = await client.exchange("", resources, { clientSecret, scopes: [scope] });
|
|
610
738
|
return token.accessToken;
|