@sipher.dev/agents 0.1.0 → 0.1.2

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.
@@ -0,0 +1,453 @@
1
+ import { A as insecureTlsForServer, C as MachineClaimError, D as discoverDeployment, a as installDeploymentConnector, b as resolveConnectorProfileContext, f as normalizeServerUrl, g as runServiceCommand, j as tlsFailureHint, k as fetchForConfiguredServer, u as removeConnectorRegistry, w as MachineClient } from "./release-resolver-PnPDodU9.js";
2
+ import { hostname } from "node:os";
3
+ import { rm } from "node:fs/promises";
4
+ import { dirname } from "node:path";
5
+ import { setTimeout } from "node:timers/promises";
6
+ //#region src/auth-client.ts
7
+ const REQUEST_TIMEOUT_MS = 3e4;
8
+ var CliAuthClient = class {
9
+ serverUrl;
10
+ tlsOptions;
11
+ constructor(serverUrl, tlsOptions = {}) {
12
+ this.serverUrl = serverUrl;
13
+ this.tlsOptions = tlsOptions;
14
+ }
15
+ async startDeviceAuthorization(input) {
16
+ const body = {
17
+ deployment_id: input.deploymentId,
18
+ connector_install_id: input.connectorInstallId,
19
+ service_key: input.serviceKey,
20
+ challenge: input.challenge,
21
+ ...input.expectedMachineId ? { expected_machine_id: input.expectedMachineId } : {},
22
+ ...input.deviceLabel ? { device_label: input.deviceLabel } : {},
23
+ ...input.bootstrap ? {
24
+ policy_digest: input.bootstrap.policyDigest,
25
+ release_tuple: input.bootstrap.releaseTuple
26
+ } : {}
27
+ };
28
+ const response = await postJson(new URL("/api/cli/oauth/device", this.serverUrl), body, this.tlsOptions);
29
+ return {
30
+ deviceCode: readString(response, "device_code"),
31
+ userCode: readString(response, "user_code"),
32
+ verificationUri: readString(response, "verification_uri"),
33
+ verificationUriComplete: readString(response, "verification_uri_complete"),
34
+ expiresIn: readNumber(response, "expires_in"),
35
+ interval: readNumber(response, "interval")
36
+ };
37
+ }
38
+ async pollDeviceToken(deviceCode) {
39
+ return parseTokenResponse(await postForm(new URL("/api/cli/oauth/token", this.serverUrl), {
40
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
41
+ device_code: deviceCode
42
+ }, this.tlsOptions));
43
+ }
44
+ async refresh(refreshToken) {
45
+ return parseTokenResponse(await postForm(new URL("/api/cli/oauth/token", this.serverUrl), {
46
+ grant_type: "refresh_token",
47
+ refresh_token: refreshToken
48
+ }, this.tlsOptions));
49
+ }
50
+ async revoke(refreshToken) {
51
+ await postForm(new URL("/api/cli/oauth/revoke", this.serverUrl), { refresh_token: refreshToken }, this.tlsOptions);
52
+ }
53
+ };
54
+ async function postJson(url, body, tlsOptions) {
55
+ const signal = AbortSignal.timeout(REQUEST_TIMEOUT_MS);
56
+ let response;
57
+ try {
58
+ response = await fetchForConfiguredServer(url, {
59
+ method: "POST",
60
+ redirect: "manual",
61
+ headers: { "content-type": "application/json" },
62
+ body: JSON.stringify(body),
63
+ signal
64
+ }, tlsOptions);
65
+ } catch (error) {
66
+ throw new Error(tlsFailureHint(error) ?? "CLI auth request failed: fetch failed.");
67
+ }
68
+ return readResponse(response);
69
+ }
70
+ async function postForm(url, body, tlsOptions) {
71
+ const signal = AbortSignal.timeout(REQUEST_TIMEOUT_MS);
72
+ let response;
73
+ try {
74
+ response = await fetchForConfiguredServer(url, {
75
+ method: "POST",
76
+ redirect: "manual",
77
+ headers: { "content-type": "application/x-www-form-urlencoded" },
78
+ body: new URLSearchParams(body),
79
+ signal
80
+ }, tlsOptions);
81
+ } catch (error) {
82
+ throw new Error(tlsFailureHint(error) ?? "CLI auth request failed: fetch failed.");
83
+ }
84
+ return readResponse(response);
85
+ }
86
+ async function readResponse(response) {
87
+ const value = await response.json().catch(() => ({}));
88
+ if (!response.ok) {
89
+ const error = isRecord(value) && typeof value.error === "string" ? value.error : "request_failed";
90
+ throw new Error(`CLI auth request failed: ${error}`);
91
+ }
92
+ if (!isRecord(value)) throw new Error("CLI auth response was invalid.");
93
+ return value;
94
+ }
95
+ function parseTokenResponse(value) {
96
+ const tokenType = readString(value, "token_type");
97
+ if (tokenType !== "Bearer") throw new Error("CLI auth token response was invalid.");
98
+ return {
99
+ accessToken: readString(value, "access_token"),
100
+ refreshToken: readString(value, "refresh_token"),
101
+ expiresIn: readNumber(value, "expires_in"),
102
+ tokenType,
103
+ deploymentId: readString(value, "deployment_id")
104
+ };
105
+ }
106
+ function readString(value, key) {
107
+ const field = value[key];
108
+ if (typeof field !== "string" || !field.trim()) throw new Error("CLI auth response was invalid.");
109
+ return field;
110
+ }
111
+ function readNumber(value, key) {
112
+ const field = value[key];
113
+ if (typeof field !== "number" || !Number.isFinite(field)) throw new Error("CLI auth response was invalid.");
114
+ return field;
115
+ }
116
+ function isRecord(value) {
117
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
118
+ }
119
+ //#endregion
120
+ //#region src/local-machine-controller.ts
121
+ const ACCESS_TOKEN_REFRESH_SKEW_MS = 3e4;
122
+ async function discoverDeploymentForStore(options) {
123
+ const knownByUrl = await options.store.findProfileForServerUrl(options.serverUrl);
124
+ const allowInsecureTls = options.allowInsecureTls ?? (knownByUrl ? insecureTlsForServer(knownByUrl.canonicalOrigin, options.serverUrl, knownByUrl.allowInsecureTls) : void 0);
125
+ const discovered = await discoverDeployment(options.serverUrl, {
126
+ allowInsecureTls,
127
+ ...knownByUrl ? {
128
+ trustedCanonicalOrigin: knownByUrl.canonicalOrigin,
129
+ expectedDeploymentId: knownByUrl.deploymentId
130
+ } : {}
131
+ });
132
+ if (knownByUrl) return discovered;
133
+ const knownById = await options.store.getProfile(discovered.deploymentId);
134
+ if (!knownById) return discovered;
135
+ return discoverDeployment(options.serverUrl, {
136
+ allowInsecureTls: options.allowInsecureTls ?? insecureTlsForServer(knownById.canonicalOrigin, options.serverUrl, knownById.allowInsecureTls),
137
+ trustedCanonicalOrigin: knownById.canonicalOrigin,
138
+ expectedDeploymentId: knownById.deploymentId
139
+ });
140
+ }
141
+ async function setupConnectorProfile(options) {
142
+ const deployment = await discoverDeploymentForStore({
143
+ serverUrl: options.serverUrl,
144
+ store: options.store,
145
+ allowInsecureTls: options.allowInsecureTls
146
+ });
147
+ const claimed = await claimLocalMachine({
148
+ deployment,
149
+ store: options.store,
150
+ name: options.name,
151
+ credentialLane: "pending",
152
+ allowInsecureTls: insecureTlsForServer(options.serverUrl, deployment.serverUrl, options.allowInsecureTls),
153
+ ...options.bootstrap ? { bootstrap: {
154
+ policyDigest: options.bootstrap.policyDigest,
155
+ releaseTuple: options.bootstrap.releaseTuple
156
+ } } : {}
157
+ });
158
+ const { machine, serverUrl, clientAccessToken } = claimed;
159
+ let profile = claimed.profile;
160
+ if (options.bootstrap) {
161
+ if (!machine.bootstrap) throw new Error("Server did not issue connector bootstrap credentials.");
162
+ profile = await options.store.updateProfile(deployment.deploymentId, (current) => ({
163
+ ...current ?? profile,
164
+ bootstrapReceipt: {
165
+ receiptId: machine.bootstrap.receiptId,
166
+ policyDigest: options.bootstrap.policyDigest,
167
+ snapshot: options.bootstrap.release.snapshot
168
+ }
169
+ }));
170
+ const credentials = {
171
+ ...machine.bootstrap,
172
+ machineId: machine.machineId
173
+ };
174
+ await installDeploymentConnector({
175
+ deployment,
176
+ release: options.bootstrap.release,
177
+ credentials,
178
+ connectorInstallId: profile.connectorInstallId,
179
+ challenge: profile.challenge,
180
+ ...profile.allowInsecureTls ? { allowInsecureTls: true } : {},
181
+ acknowledge: (receiptId) => new MachineClient(serverUrl, { allowInsecureTls: profile.allowInsecureTls }).acknowledgeBootstrap({
182
+ accessToken: clientAccessToken,
183
+ receiptId
184
+ }),
185
+ ...options.reconciliationLockHeld ? { reconciliationLockHeld: true } : {}
186
+ });
187
+ await options.store.clearBootstrapReceipt(deployment.deploymentId);
188
+ }
189
+ if (options.installService) {
190
+ if (!options.serviceInstaller) throw new Error("Connector service install is not available in this runtime.");
191
+ await options.serviceInstaller({
192
+ serverUrl,
193
+ deploymentId: profile.deploymentId,
194
+ serviceKey: profile.serviceKey,
195
+ allowInsecureTls: profile.allowInsecureTls
196
+ });
197
+ }
198
+ return machine;
199
+ }
200
+ async function resumeConnectorBootstrap(options) {
201
+ const stored = await options.store.getProfile(options.deployment.deploymentId);
202
+ const receipt = stored?.bootstrapReceipt;
203
+ if (!stored?.refreshToken || !receipt) throw new Error("Recoverable connector bootstrap receipt was not found.");
204
+ const claimed = await claimLocalMachine({
205
+ deployment: options.deployment,
206
+ store: options.store,
207
+ credentialLane: "active",
208
+ allowInsecureTls: options.allowInsecureTls,
209
+ bootstrap: {
210
+ policyDigest: receipt.policyDigest,
211
+ releaseTuple: receipt.snapshot.tuple,
212
+ receiptId: receipt.receiptId
213
+ }
214
+ });
215
+ if (!claimed.machine.bootstrap) throw new Error("Server did not recover connector bootstrap credentials.");
216
+ const nextReceipt = claimed.machine.bootstrap.receiptId;
217
+ await options.store.updateProfile(options.deployment.deploymentId, (current) => ({
218
+ ...current ?? claimed.profile,
219
+ bootstrapReceipt: {
220
+ receiptId: nextReceipt,
221
+ policyDigest: receipt.policyDigest,
222
+ snapshot: receipt.snapshot
223
+ }
224
+ }));
225
+ await installDeploymentConnector({
226
+ deployment: options.deployment,
227
+ release: options.release,
228
+ credentials: {
229
+ ...claimed.machine.bootstrap,
230
+ machineId: claimed.machine.machineId
231
+ },
232
+ connectorInstallId: claimed.profile.connectorInstallId,
233
+ challenge: claimed.profile.challenge,
234
+ ...claimed.profile.allowInsecureTls ? { allowInsecureTls: true } : {},
235
+ acknowledge: (receiptId) => new MachineClient(options.deployment.serverUrl, { allowInsecureTls: claimed.profile.allowInsecureTls }).acknowledgeBootstrap({
236
+ accessToken: claimed.clientAccessToken,
237
+ receiptId
238
+ }),
239
+ ...options.reconciliationLockHeld ? { reconciliationLockHeld: true } : {}
240
+ });
241
+ await options.store.clearBootstrapReceipt(options.deployment.deploymentId);
242
+ return claimed.machine;
243
+ }
244
+ async function repairConnectorProfile(options) {
245
+ const deployment = options.deployment ?? await discoverDeploymentForStore({
246
+ serverUrl: options.serverUrl,
247
+ store: options.store,
248
+ allowInsecureTls: options.allowInsecureTls
249
+ });
250
+ const storedProfile = await options.store.getProfile(deployment.deploymentId);
251
+ return claimLocalMachine({
252
+ deployment,
253
+ store: options.store,
254
+ name: options.name,
255
+ credentialLane: "active",
256
+ allowInsecureTls: insecureTlsForServer(options.serverUrl, deployment.serverUrl, options.allowInsecureTls ?? storedProfile?.allowInsecureTls)
257
+ });
258
+ }
259
+ async function startConnectorDeviceAuthorization(options) {
260
+ const deployment = await discoverDeploymentForStore({
261
+ serverUrl: options.serverUrl,
262
+ store: options.store,
263
+ allowInsecureTls: options.allowInsecureTls
264
+ });
265
+ const allowInsecureTls = insecureTlsForServer(options.serverUrl, deployment.serverUrl, options.allowInsecureTls);
266
+ const profile = await options.store.prepareDeployment(deployment, { allowInsecureTls });
267
+ return {
268
+ ...await new CliAuthClient(deployment.serverUrl, { allowInsecureTls }).startDeviceAuthorization({
269
+ deploymentId: deployment.deploymentId,
270
+ connectorInstallId: profile.connectorInstallId,
271
+ serviceKey: deployment.serviceKey,
272
+ challenge: profile.challenge,
273
+ expectedMachineId: profile.machineId,
274
+ deviceLabel: options.deviceLabel ?? hostname(),
275
+ ...options.bootstrap ? { bootstrap: options.bootstrap } : {}
276
+ }),
277
+ serverUrl: deployment.serverUrl,
278
+ deploymentId: deployment.deploymentId
279
+ };
280
+ }
281
+ async function pollConnectorDeviceToken(options) {
282
+ const token = await pollDeviceToken(new CliAuthClient(normalizeServerUrl(options.serverUrl), { allowInsecureTls: options.allowInsecureTls }), {
283
+ deviceCode: options.deviceCode,
284
+ intervalSeconds: options.intervalSeconds,
285
+ expiresInSeconds: options.expiresInSeconds
286
+ });
287
+ if (token.deploymentId !== options.deploymentId) throw new Error("Authenticated token grant belongs to a different deployment.");
288
+ return options.store.savePendingLogin({
289
+ deploymentId: options.deploymentId,
290
+ refreshToken: token.refreshToken,
291
+ accessToken: token.accessToken,
292
+ expiresInSeconds: token.expiresIn
293
+ });
294
+ }
295
+ async function ensureAccessToken(input) {
296
+ const serverUrl = normalizeServerUrl(input.serverUrl);
297
+ return input.store.withLockedData(async (data) => {
298
+ const now = input.now ?? /* @__PURE__ */ new Date();
299
+ const pendingProfile = data.pendingProfiles[input.deploymentId];
300
+ const activeProfile = data.profiles[input.deploymentId];
301
+ const pending = data.pendingLogins[input.deploymentId];
302
+ const profile = input.usePendingCredentials ? pendingProfile : activeProfile;
303
+ const credentials = input.usePendingCredentials ? pending : activeProfile;
304
+ if (!profile) throw new Error(`Not logged in to ${serverUrl}. Run agents login.`);
305
+ if (!credentials?.refreshToken) throw new Error(`Not logged in to ${serverUrl}. Run agents login.`);
306
+ const auth = input.authClient ?? new CliAuthClient(serverUrl, { allowInsecureTls: input.allowInsecureTls });
307
+ if (!input.forceRefresh && credentials.accessToken && tokenIsFresh(credentials.accessTokenExpiresAt, now)) return credentials.accessToken;
308
+ const refreshed = await auth.refresh(credentials.refreshToken);
309
+ if (refreshed.deploymentId !== profile.deploymentId) throw new Error("Authenticated token grant belongs to a different deployment.");
310
+ const updated = {
311
+ ...credentials,
312
+ refreshToken: refreshed.refreshToken,
313
+ accessToken: refreshed.accessToken,
314
+ accessTokenExpiresAt: expiresAt(refreshed.expiresIn, now)
315
+ };
316
+ if (input.usePendingCredentials) data.pendingLogins[input.deploymentId] = updated;
317
+ else data.profiles[input.deploymentId] = {
318
+ ...activeProfile,
319
+ ...updated
320
+ };
321
+ return refreshed.accessToken;
322
+ });
323
+ }
324
+ function createAccessTokenProvider(input) {
325
+ return async (options) => {
326
+ const deployment = await discoverDeploymentForStore({
327
+ serverUrl: input.serverUrl,
328
+ store: input.store,
329
+ allowInsecureTls: input.allowInsecureTls
330
+ });
331
+ if (deployment.deploymentId !== input.deploymentId) throw new Error("Discovered deployment identity changed before connector authentication.");
332
+ const allowInsecureTls = insecureTlsForServer(input.serverUrl, deployment.serverUrl, input.allowInsecureTls);
333
+ const accessToken = await ensureAccessToken({
334
+ ...input,
335
+ serverUrl: deployment.serverUrl,
336
+ forceRefresh: options?.forceRefresh,
337
+ allowInsecureTls
338
+ });
339
+ return {
340
+ serverUrl: deployment.serverUrl,
341
+ accessToken,
342
+ ...allowInsecureTls ? { allowInsecureTls: true } : {}
343
+ };
344
+ };
345
+ }
346
+ async function claimLocalMachine(options) {
347
+ const deployment = options.deployment;
348
+ const serverUrl = deployment.serverUrl;
349
+ const prepared = options.credentialLane === "pending" ? await options.store.prepareDeployment(deployment, { allowInsecureTls: options.allowInsecureTls }) : await options.store.getProfile(deployment.deploymentId);
350
+ if (!prepared || options.credentialLane === "active" && !prepared.refreshToken) throw new Error(`Not logged in to ${serverUrl}. Run agents login.`);
351
+ const accessToken = await ensureAccessToken({
352
+ serverUrl,
353
+ deploymentId: deployment.deploymentId,
354
+ store: options.store,
355
+ usePendingCredentials: options.credentialLane === "pending",
356
+ allowInsecureTls: options.allowInsecureTls
357
+ }).catch((error) => {
358
+ throw markRefreshError(error);
359
+ });
360
+ const machineClient = new MachineClient(serverUrl, { allowInsecureTls: options.allowInsecureTls });
361
+ const claimInput = {
362
+ accessToken,
363
+ deploymentId: deployment.deploymentId,
364
+ serviceKey: deployment.serviceKey,
365
+ challenge: prepared.challenge,
366
+ connectorInstallId: prepared.connectorInstallId,
367
+ name: options.name,
368
+ ...options.bootstrap ? { bootstrap: options.bootstrap } : {}
369
+ };
370
+ let machine;
371
+ try {
372
+ machine = await machineClient.claim(claimInput);
373
+ } catch (error) {
374
+ if (error instanceof MachineClaimError && error.code === "machine_retirement_pending") {
375
+ const retirement = await machineClient.readRetirement(accessToken);
376
+ if (!retirement || retirement.deploymentId !== deployment.deploymentId || retirement.connectorInstallId !== prepared.connectorInstallId) throw new Error("Machine retirement did not match the local deployment binding.");
377
+ await clearRetiredLocalBinding(deployment.deploymentId, deployment.serviceKey);
378
+ if (!retirement.acknowledged) await machineClient.acknowledgeRetirement({
379
+ accessToken,
380
+ retirement
381
+ });
382
+ machine = await machineClient.claim(claimInput);
383
+ } else {
384
+ if (!options.bootstrap || error instanceof MachineClaimError && error.status < 500) throw error;
385
+ machine = await machineClient.claim(claimInput);
386
+ }
387
+ }
388
+ const profile = options.credentialLane === "pending" ? await options.store.commitPendingDeployment(deployment.deploymentId, machine.machineId) : await options.store.updateProfile(deployment.deploymentId, (current) => {
389
+ if (!current?.refreshToken) throw new Error(`Not logged in to ${serverUrl}. Run agents login.`);
390
+ const updated = {
391
+ ...current,
392
+ serverUrl,
393
+ canonicalOrigin: deployment.canonicalOrigin,
394
+ aliases: deployment.aliases,
395
+ policyDigest: deployment.policyDigest,
396
+ serviceKey: deployment.serviceKey,
397
+ machineId: machine.machineId
398
+ };
399
+ delete updated.allowInsecureTls;
400
+ if (options.allowInsecureTls) updated.allowInsecureTls = true;
401
+ return updated;
402
+ });
403
+ return {
404
+ machine,
405
+ serverUrl,
406
+ profile,
407
+ clientAccessToken: accessToken
408
+ };
409
+ }
410
+ async function clearRetiredLocalBinding(deploymentId, serviceKey) {
411
+ const context = resolveConnectorProfileContext(serviceKey);
412
+ await runServiceCommand("uninstall", {
413
+ deploymentId,
414
+ serviceKey
415
+ });
416
+ await Promise.all([
417
+ removeConnectorRegistry(context),
418
+ rm(dirname(context.configPath), {
419
+ recursive: true,
420
+ force: true
421
+ }),
422
+ rm(context.statePath, {
423
+ recursive: true,
424
+ force: true
425
+ })
426
+ ]);
427
+ }
428
+ async function pollDeviceToken(auth, input) {
429
+ const expiresAt = Date.now() + input.expiresInSeconds * 1e3;
430
+ let intervalMs = Math.max(1, input.intervalSeconds) * 1e3;
431
+ while (Date.now() < expiresAt) try {
432
+ return await auth.pollDeviceToken(input.deviceCode);
433
+ } catch (error) {
434
+ const message = error instanceof Error ? error.message : "";
435
+ if (message.includes("slow_down")) intervalMs += 5e3;
436
+ if (!message.includes("authorization_pending") && !message.includes("slow_down")) throw error;
437
+ await setTimeout(intervalMs);
438
+ }
439
+ throw new Error("CLI login expired before authorization completed.");
440
+ }
441
+ function tokenIsFresh(expiresAtValue, now) {
442
+ if (!expiresAtValue) return false;
443
+ return new Date(expiresAtValue).getTime() - now.getTime() > ACCESS_TOKEN_REFRESH_SKEW_MS;
444
+ }
445
+ function expiresAt(expiresInSeconds, now) {
446
+ return new Date(now.getTime() + Math.max(0, expiresInSeconds) * 1e3).toISOString();
447
+ }
448
+ function markRefreshError(error) {
449
+ if (error instanceof Error) return Object.assign(error, { localMachineStage: "refresh" });
450
+ return Object.assign(/* @__PURE__ */ new Error("Failed to refresh connector access token."), { localMachineStage: "refresh" });
451
+ }
452
+ //#endregion
453
+ export { repairConnectorProfile as a, startConnectorDeviceAuthorization as c, pollConnectorDeviceToken as i, CliAuthClient as l, discoverDeploymentForStore as n, resumeConnectorBootstrap as o, ensureAccessToken as r, setupConnectorProfile as s, createAccessTokenProvider as t };