@forgezero/agent 0.1.39 → 0.1.40

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.
@@ -86,162 +86,14 @@ export declare function removeCloudflareWarpDatabaseInclude(config: Omit<Cloudfl
86
86
  /** Reconcile one remotely-managed tunnel route and its proxied DNS record. */
87
87
  export declare function configureCloudflareEdge(config: CloudflareEdgeConfig, fetcher?: typeof fetch): Promise<void>;
88
88
  export interface CloudflareApiTokens {
89
- /** One scoped token may perform every operation. */
89
+ /** Backward-compatible single-token input; two scoped tokens are preferred. */
90
90
  apiToken?: string;
91
- /** Or split tokens may constrain each operation independently. */
91
+ /** Attended management token mapped to Tunnel and DNS operations. */
92
92
  tunnelApiToken?: string;
93
93
  dnsApiToken?: string;
94
+ /** Runtime token used only for KV node-directory writes. */
94
95
  kvApiToken?: string;
95
- accessApiToken?: string;
96
- workerApiToken?: string;
97
96
  }
98
- export interface CloudflareKvNamespace {
99
- id: string;
100
- title: string;
101
- }
102
- export interface CloudflareAccessServiceToken {
103
- id: string;
104
- name: string;
105
- client_id: string;
106
- client_secret?: string;
107
- duration?: string;
108
- }
109
- export interface CloudflareAccessCredentials {
110
- tokenId: string;
111
- clientId: string;
112
- clientSecret: string;
113
- }
114
- export interface CloudflareAccountRuntimeToken {
115
- id: string;
116
- value: string;
117
- name: string;
118
- permissionNames: string[];
119
- }
120
- /**
121
- * Create one account-owned API token from exact, dynamically resolved account
122
- * permission groups. The value is returned once for immediate 0600 checkpoint.
123
- */
124
- export declare function createCloudflareAccountRuntimeToken(config: {
125
- accountId: string;
126
- name: string;
127
- permissionNames: readonly string[];
128
- apiToken: string;
129
- }, fetcher?: typeof fetch): Promise<CloudflareAccountRuntimeToken>;
130
- /**
131
- * Create the Worker's Access service token, or prove the caller supplied the
132
- * sealed secret for the existing token. Cloudflare intentionally returns the
133
- * client secret only once, so silently "reusing" an existing row without its
134
- * secret would configure a Worker that can never authenticate.
135
- */
136
- export declare function ensureCloudflareAccessServiceToken(config: {
137
- accountId: string;
138
- name: string;
139
- duration?: string;
140
- apiToken: string;
141
- existing?: CloudflareAccessCredentials;
142
- }, fetcher?: typeof fetch): Promise<{
143
- credentials: CloudflareAccessCredentials;
144
- created: boolean;
145
- }>;
146
- interface CloudflareAccessPolicy {
147
- id: string;
148
- name: string;
149
- decision?: string;
150
- include?: unknown[];
151
- }
152
- /** Create or update one reusable Service Auth policy for the Worker token. */
153
- export declare function ensureCloudflareAccessPolicy(config: {
154
- accountId: string;
155
- name: string;
156
- serviceTokenId: string;
157
- apiToken: string;
158
- }, fetcher?: typeof fetch): Promise<{
159
- policy: CloudflareAccessPolicy;
160
- created: boolean;
161
- }>;
162
- interface CloudflareAccessApplication {
163
- id: string;
164
- name: string;
165
- type?: string;
166
- domain?: string;
167
- self_hosted_domains?: string[];
168
- }
169
- /** Protect one node hostname so only the public Worker can reach it. */
170
- export declare function ensureCloudflareAccessApplication(config: {
171
- accountId: string;
172
- name: string;
173
- hostname: string;
174
- policyId: string;
175
- apiToken: string;
176
- }, fetcher?: typeof fetch): Promise<{
177
- application: CloudflareAccessApplication;
178
- created: boolean;
179
- }>;
180
- /** Attach the same non-interactive service-auth policy to WARP enrollment. */
181
- export declare function ensureCloudflareWarpEnrollmentApplication(config: {
182
- accountId: string;
183
- name: string;
184
- policyId: string;
185
- apiToken: string;
186
- }, fetcher?: typeof fetch): Promise<{
187
- application: CloudflareAccessApplication;
188
- created: boolean;
189
- }>;
190
- export interface CloudflareVirtualNetwork {
191
- id: string;
192
- name: string;
193
- comment?: string;
194
- is_default_network?: boolean;
195
- deleted_at?: string | null;
196
- }
197
- /** Create or reuse one named VNET; never silently adopt an ambiguous/deleted row. */
198
- export declare function ensureCloudflareVirtualNetwork(config: {
199
- accountId: string;
200
- name: string;
201
- comment: string;
202
- apiToken: string;
203
- }, fetcher?: typeof fetch): Promise<{
204
- virtualNetwork: CloudflareVirtualNetwork;
205
- created: boolean;
206
- }>;
207
- interface CloudflareDevicePolicy {
208
- id: string;
209
- name: string;
210
- match?: string;
211
- precedence?: number;
212
- }
213
- /**
214
- * Create a locked WARP device profile selected only by this service token.
215
- * Split-tunnel includes are reconciled separately so existing owned entries
216
- * remain visible and reviewable.
217
- */
218
- export declare function ensureCloudflareWarpDevicePolicy(config: {
219
- accountId: string;
220
- name: string;
221
- serviceTokenId: string;
222
- virtualNetworkId: string;
223
- precedence?: number;
224
- apiToken: string;
225
- }, fetcher?: typeof fetch): Promise<{
226
- policy: CloudflareDevicePolicy;
227
- created: boolean;
228
- }>;
229
- /** Atomically replace both Worker-side Access credentials. */
230
- export declare function configureCloudflareWorkerAccessSecrets(config: {
231
- accountId: string;
232
- scriptName: string;
233
- credentials: CloudflareAccessCredentials;
234
- apiToken: string;
235
- }, fetcher?: typeof fetch): Promise<void>;
236
- /** Create or reuse exactly one named Worker KV namespace. */
237
- export declare function ensureCloudflareKvNamespace(config: {
238
- accountId: string;
239
- title: string;
240
- apiToken: string;
241
- }, fetcher?: typeof fetch): Promise<{
242
- namespace: CloudflareKvNamespace;
243
- created: boolean;
244
- }>;
245
97
  export interface CloudflareTunnel {
246
98
  id: string;
247
99
  name: string;
@@ -130,23 +130,32 @@ async function configureCloudflareEdge(config, fetcher = fetch) {
130
130
  const tunnelAuth = { apiToken: config.tunnelApiToken?.trim() || config.apiToken };
131
131
  const dnsAuth = { apiToken: config.dnsApiToken?.trim() || config.apiToken };
132
132
  const tunnelPath = `/accounts/${config.accountId}/cfd_tunnel/${config.tunnelId}/configurations`;
133
- const current = await cf(tunnelAuth, tunnelPath, {}, fetcher);
134
- const existing = current.config?.ingress ?? [];
135
- const catchAll = existing.filter((rule) => !("hostname" in rule));
136
- const otherHosts = existing.filter((rule) => ("hostname" in rule) && rule.hostname !== config.hostname);
137
- await cf(tunnelAuth, tunnelPath, {
138
- method: "PUT",
139
- body: JSON.stringify({ config: { ingress: [
140
- { hostname: config.hostname, service: config.service },
141
- ...otherHosts,
142
- ...catchAll.length > 0 ? catchAll : [{ service: "http_status:404" }]
143
- ] } })
144
- }, fetcher);
145
133
  const dnsPath = `/zones/${config.zoneId}/dns_records`;
146
- const records = await cf(dnsAuth, `${dnsPath}?type=CNAME&name=${encodeURIComponent(config.hostname)}&per_page=1000`, {}, fetcher);
134
+ const [current, records] = await Promise.all([
135
+ cf(tunnelAuth, tunnelPath, {}, fetcher),
136
+ cf(dnsAuth, `${dnsPath}?name=${encodeURIComponent(config.hostname)}&per_page=1000`, {}, fetcher)
137
+ ]);
147
138
  if (records.length > 1) {
148
139
  throw new Error(`Cloudflare DNS record for ${config.hostname} is ambiguous`);
149
140
  }
141
+ const existingRecord = records[0];
142
+ if (existingRecord && (existingRecord.type !== "CNAME" || existingRecord.name && existingRecord.name.toLowerCase() !== config.hostname.toLowerCase())) {
143
+ throw new Error(`Cloudflare DNS hostname ${config.hostname} is already owned by an incompatible ${existingRecord.type ?? "unknown"} record`);
144
+ }
145
+ const existing = current.config?.ingress ?? [];
146
+ const catchAll = existing.filter((rule) => !("hostname" in rule));
147
+ const otherHosts = existing.filter((rule) => ("hostname" in rule) && rule.hostname !== config.hostname);
148
+ const desiredIngress = [
149
+ { hostname: config.hostname, service: config.service },
150
+ ...otherHosts,
151
+ ...catchAll.length > 0 ? catchAll : [{ service: "http_status:404" }]
152
+ ];
153
+ if (JSON.stringify(existing) !== JSON.stringify(desiredIngress)) {
154
+ await cf(tunnelAuth, tunnelPath, {
155
+ method: "PUT",
156
+ body: JSON.stringify({ config: { ingress: desiredIngress } })
157
+ }, fetcher);
158
+ }
150
159
  const record = {
151
160
  type: "CNAME",
152
161
  name: config.hostname,
@@ -154,233 +163,13 @@ async function configureCloudflareEdge(config, fetcher = fetch) {
154
163
  proxied: true,
155
164
  ttl: 1
156
165
  };
157
- await cf(dnsAuth, records[0] ? `${dnsPath}/${records[0].id}` : dnsPath, {
158
- method: records[0] ? "PUT" : "POST",
159
- body: JSON.stringify(record)
160
- }, fetcher);
161
- }
162
- var exactAccountPermissionGroup = async (config, name, fetcher) => {
163
- const groups = await cf(config, `/accounts/${config.accountId}/tokens/permission_groups?name=${encodeURIComponent(name)}` + "&scope=com.cloudflare.api.account", {}, fetcher);
164
- const matches = groups.filter((group) => group.name === name && group.scopes?.includes("com.cloudflare.api.account") && Boolean(group.id && /^[a-f0-9]{32}$/i.test(group.id)));
165
- if (matches.length !== 1) {
166
- throw new Error(`Cloudflare account token permission group ${name} is ${matches.length === 0 ? "missing" : "ambiguous"}`);
167
- }
168
- return { id: matches[0].id, name };
169
- };
170
- async function createCloudflareAccountRuntimeToken(config, fetcher = fetch) {
171
- if (!/^[a-f0-9]{32}$/i.test(config.accountId))
172
- throw new Error("Cloudflare account id is invalid");
173
- const name = config.name.trim();
174
- if (!/^[A-Za-z0-9][A-Za-z0-9 ._-]{0,119}$/.test(name)) {
175
- throw new Error("Cloudflare account runtime-token name is invalid");
176
- }
177
- const permissionNames = [...new Set(config.permissionNames)];
178
- if (permissionNames.length === 0)
179
- throw new Error("Cloudflare account runtime token needs a permission group");
180
- const permissionGroups = await Promise.all(permissionNames.map((permissionName) => exactAccountPermissionGroup(config, permissionName, fetcher)));
181
- const body = {
182
- name,
183
- policies: [{
184
- effect: "allow",
185
- permission_groups: permissionGroups.map(({ id }) => ({ id })),
186
- resources: { [`com.cloudflare.api.account.${config.accountId}`]: "*" }
187
- }]
188
- };
189
- const created = await cf(config, `/accounts/${config.accountId}/tokens`, { method: "POST", body: JSON.stringify(body) }, fetcher);
190
- if (!created.id || !/^[a-f0-9]{32}$/i.test(created.id) || !created.value || !/^[A-Za-z0-9._-]{40,80}$/.test(created.value)) {
191
- throw new Error("Cloudflare did not return the one-time account runtime-token id and value");
192
- }
193
- return { id: created.id, value: created.value, name, permissionNames };
194
- }
195
- async function ensureCloudflareAccessServiceToken(config, fetcher = fetch) {
196
- const name = config.name.trim();
197
- if (!/^[A-Za-z0-9][A-Za-z0-9 ._-]{0,99}$/.test(name)) {
198
- throw new Error("Cloudflare Access service-token name is invalid");
199
- }
200
- const path = `/accounts/${config.accountId}/access/service_tokens`;
201
- const tokens = await cf(config, `${path}?per_page=1000`, {}, fetcher);
202
- const matches = tokens.filter((token) => token.name === name);
203
- if (matches.length > 1)
204
- throw new Error(`Cloudflare Access service token ${name} is ambiguous`);
205
- if (matches[0]) {
206
- if (!config.existing || config.existing.tokenId !== matches[0].id || config.existing.clientId !== matches[0].client_id || !config.existing.clientSecret) {
207
- throw new Error(`Cloudflare Access service token ${name} exists but its one-time client secret was not supplied`);
208
- }
209
- return { credentials: config.existing, created: false };
210
- }
211
- const created = await cf(config, path, {
212
- method: "POST",
213
- body: JSON.stringify({ name, duration: config.duration ?? "8760h" })
214
- }, fetcher);
215
- if (!created.id || !created.client_id || !created.client_secret) {
216
- throw new Error("Cloudflare did not return the new Access service-token secret");
166
+ const dnsAlreadyCorrect = existingRecord?.type === record.type && existingRecord.name?.toLowerCase() === record.name.toLowerCase() && existingRecord.content?.toLowerCase() === record.content.toLowerCase() && existingRecord.proxied === true && existingRecord.ttl === 1;
167
+ if (!dnsAlreadyCorrect) {
168
+ await cf(dnsAuth, existingRecord ? `${dnsPath}/${encodeURIComponent(existingRecord.id)}` : dnsPath, {
169
+ method: existingRecord ? "PUT" : "POST",
170
+ body: JSON.stringify(record)
171
+ }, fetcher);
217
172
  }
218
- return {
219
- credentials: {
220
- tokenId: created.id,
221
- clientId: created.client_id,
222
- clientSecret: created.client_secret
223
- },
224
- created: true
225
- };
226
- }
227
- async function ensureCloudflareAccessPolicy(config, fetcher = fetch) {
228
- const path = `/accounts/${config.accountId}/access/policies`;
229
- const policies = await cf(config, `${path}?per_page=1000`, {}, fetcher);
230
- const matches = policies.filter((policy2) => policy2.name === config.name);
231
- if (matches.length > 1)
232
- throw new Error(`Cloudflare Access policy ${config.name} is ambiguous`);
233
- const desired = {
234
- name: config.name,
235
- decision: "non_identity",
236
- include: [{ service_token: { token_id: config.serviceTokenId } }]
237
- };
238
- const policy = await cf(config, matches[0] ? `${path}/${encodeURIComponent(matches[0].id)}` : path, {
239
- method: matches[0] ? "PUT" : "POST",
240
- body: JSON.stringify(desired)
241
- }, fetcher);
242
- return { policy, created: !matches[0] };
243
- }
244
- async function ensureCloudflareAccessApplication(config, fetcher = fetch) {
245
- const path = `/accounts/${config.accountId}/access/apps`;
246
- const applications = await cf(config, `${path}?per_page=1000`, {}, fetcher);
247
- const matches = applications.filter((application2) => application2.domain === config.hostname || application2.self_hosted_domains?.includes(config.hostname));
248
- if (matches.length > 1)
249
- throw new Error(`Cloudflare Access application for ${config.hostname} is ambiguous`);
250
- const desired = {
251
- name: config.name,
252
- type: "self_hosted",
253
- domain: config.hostname,
254
- session_duration: "24h",
255
- service_auth_401_redirect: true,
256
- policies: [{ id: config.policyId, precedence: 1 }]
257
- };
258
- const application = await cf(config, matches[0] ? `${path}/${encodeURIComponent(matches[0].id)}` : path, { method: matches[0] ? "PUT" : "POST", body: JSON.stringify(desired) }, fetcher);
259
- return { application, created: !matches[0] };
260
- }
261
- async function ensureCloudflareWarpEnrollmentApplication(config, fetcher = fetch) {
262
- const name = config.name.trim();
263
- if (!/^[A-Za-z0-9][A-Za-z0-9 ._-]{0,99}$/.test(name)) {
264
- throw new Error("Cloudflare WARP enrollment application name is invalid");
265
- }
266
- const path = `/accounts/${config.accountId}/access/apps`;
267
- const applications = await cf(config, `${path}?per_page=1000`, {}, fetcher);
268
- const matches = applications.filter((application2) => application2.type === "warp" || application2.name === name);
269
- if (matches.length > 1)
270
- throw new Error(`Cloudflare WARP enrollment application ${name} is ambiguous`);
271
- if (matches[0] && (matches[0].type !== "warp" || matches[0].name !== name)) {
272
- throw new Error(`Cloudflare Access application ${name} is not the owned WARP enrollment application`);
273
- }
274
- const desired = {
275
- name,
276
- type: "warp",
277
- policies: [{ id: config.policyId, precedence: 1 }]
278
- };
279
- const application = await cf(config, matches[0] ? `${path}/${encodeURIComponent(matches[0].id)}` : path, { method: matches[0] ? "PUT" : "POST", body: JSON.stringify(desired) }, fetcher);
280
- if (!application.id || application.type && application.type !== "warp") {
281
- throw new Error("Cloudflare did not return the WARP enrollment application");
282
- }
283
- return { application, created: !matches[0] };
284
- }
285
- async function ensureCloudflareVirtualNetwork(config, fetcher = fetch) {
286
- const name = config.name.trim();
287
- if (!/^[A-Za-z0-9][A-Za-z0-9 ._-]{0,99}$/.test(name))
288
- throw new Error("Cloudflare VNET name is invalid");
289
- const path = `/accounts/${config.accountId}/teamnet/virtual_networks`;
290
- const networks = await cf(config, `${path}?per_page=1000`, {}, fetcher);
291
- const matches = networks.filter((network) => !network.deleted_at && network.name === name);
292
- if (matches.length > 1)
293
- throw new Error(`Cloudflare VNET ${name} is ambiguous`);
294
- if (matches[0])
295
- return { virtualNetwork: matches[0], created: false };
296
- const virtualNetwork = await cf(config, path, {
297
- method: "POST",
298
- body: JSON.stringify({ name, comment: config.comment.slice(0, 256), is_default_network: false })
299
- }, fetcher);
300
- if (!virtualNetwork.id || !/^[0-9a-f-]{36}$/i.test(virtualNetwork.id)) {
301
- throw new Error("Cloudflare did not return the VNET id");
302
- }
303
- return { virtualNetwork, created: true };
304
- }
305
- async function ensureCloudflareWarpDevicePolicy(config, fetcher = fetch) {
306
- const name = config.name.trim();
307
- if (!/^[A-Za-z0-9][A-Za-z0-9 ._-]{0,99}$/.test(name))
308
- throw new Error("Cloudflare WARP device profile name is invalid");
309
- if (!/^[A-Za-z0-9_-]{1,128}$/.test(config.serviceTokenId))
310
- throw new Error("Cloudflare service-token id is invalid");
311
- if (!/^[0-9a-f-]{36}$/i.test(config.virtualNetworkId))
312
- throw new Error("Cloudflare VNET id is invalid");
313
- const precedence = config.precedence ?? 100;
314
- if (!Number.isInteger(precedence) || precedence < 1 || precedence > 999999) {
315
- throw new Error("Cloudflare WARP device profile precedence is invalid");
316
- }
317
- const match = `identity.service_token_uuid == "${config.serviceTokenId}"`;
318
- const listPath = `/accounts/${config.accountId}/devices/policies`;
319
- const path = `/accounts/${config.accountId}/devices/policy`;
320
- const policies = await cf(config, `${listPath}?per_page=1000`, {}, fetcher);
321
- const matches = policies.filter((policy2) => policy2.name === name);
322
- if (matches.length > 1)
323
- throw new Error(`Cloudflare WARP device profile ${name} is ambiguous`);
324
- if (matches[0]?.match && matches[0].match !== match) {
325
- throw new Error(`Cloudflare WARP device profile ${name} belongs to another enrollment identity`);
326
- }
327
- const desired = {
328
- name,
329
- match,
330
- precedence,
331
- description: "ForgeZero non-interactive compute enrollment",
332
- enabled: true,
333
- allow_mode_switch: false,
334
- allowed_to_leave: false,
335
- auto_connect: 0,
336
- switch_locked: true,
337
- service_mode_v2: { mode: "warp" },
338
- virtual_networks: { allowed: [config.virtualNetworkId], default: config.virtualNetworkId }
339
- };
340
- const policy = await cf(config, matches[0] ? `${path}/${encodeURIComponent(matches[0].id)}` : path, { method: matches[0] ? "PATCH" : "POST", body: JSON.stringify(desired) }, fetcher);
341
- if (!policy.id)
342
- throw new Error("Cloudflare did not return the WARP device profile id");
343
- return { policy, created: !matches[0] };
344
- }
345
- async function configureCloudflareWorkerAccessSecrets(config, fetcher = fetch) {
346
- if (!/^[a-z][a-z0-9-]{0,62}$/.test(config.scriptName)) {
347
- throw new Error("Cloudflare Worker script name is invalid");
348
- }
349
- await cf(config, `/accounts/${config.accountId}/workers/scripts/${config.scriptName}/secrets-bulk`, {
350
- method: "PATCH",
351
- body: JSON.stringify({
352
- secrets: {
353
- CF_ACCESS_CLIENT_ID: {
354
- name: "CF_ACCESS_CLIENT_ID",
355
- type: "secret_text",
356
- text: config.credentials.clientId
357
- },
358
- CF_ACCESS_CLIENT_SECRET: {
359
- name: "CF_ACCESS_CLIENT_SECRET",
360
- type: "secret_text",
361
- text: config.credentials.clientSecret
362
- }
363
- }
364
- })
365
- }, fetcher);
366
- }
367
- async function ensureCloudflareKvNamespace(config, fetcher = fetch) {
368
- const title = config.title.trim();
369
- if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(title)) {
370
- throw new Error("Cloudflare KV namespace title is invalid");
371
- }
372
- const path = `/accounts/${config.accountId}/storage/kv/namespaces`;
373
- const namespaces = await cf(config, `${path}?per_page=1000`, {}, fetcher);
374
- const matches = namespaces.filter((namespace2) => namespace2.title === title);
375
- if (matches.length > 1)
376
- throw new Error(`Cloudflare KV namespace ${title} is ambiguous`);
377
- if (matches[0])
378
- return { namespace: matches[0], created: false };
379
- const namespace = await cf(config, path, {
380
- method: "POST",
381
- body: JSON.stringify({ title })
382
- }, fetcher);
383
- return { namespace, created: true };
384
173
  }
385
174
  async function ensureCloudflareTunnel(config, fetcher = fetch) {
386
175
  const name = config.name.trim();
@@ -407,18 +196,9 @@ export {
407
196
  removeCloudflareWarpDatabaseInclude,
408
197
  removeCloudflarePrivateRoute,
409
198
  removeCloudflarePrivateDatabaseRoute,
410
- ensureCloudflareWarpEnrollmentApplication,
411
- ensureCloudflareWarpDevicePolicy,
412
199
  ensureCloudflareWarpDatabaseInclude,
413
- ensureCloudflareVirtualNetwork,
414
200
  ensureCloudflareTunnel,
415
201
  ensureCloudflarePrivateRoute,
416
202
  ensureCloudflarePrivateDatabaseRoute,
417
- ensureCloudflareKvNamespace,
418
- ensureCloudflareAccessServiceToken,
419
- ensureCloudflareAccessPolicy,
420
- ensureCloudflareAccessApplication,
421
- createCloudflareAccountRuntimeToken,
422
- configureCloudflareWorkerAccessSecrets,
423
203
  configureCloudflareEdge
424
204
  };
@@ -0,0 +1,60 @@
1
+ /**
2
+ * One credential policy for every place the Agent can run.
3
+ *
4
+ * Operator credentials are attended input files and are never installed as a
5
+ * runtime source. Physical-metal credentials are fixed host identities loaded
6
+ * only by systemd. Enrolled platform and tenant computes read their assigned
7
+ * project/environment Vault replica first and may use only an explicitly
8
+ * installed, same-name systemd credential while that replica cannot answer.
9
+ */
10
+ export declare const AGENT_CREDENTIAL_LOCATIONS: readonly ["operator", "metal", "platform-compute", "tenant-compute"];
11
+ export type AgentCredentialLocation = (typeof AGENT_CREDENTIAL_LOCATIONS)[number];
12
+ export declare const AGENT_CREDENTIAL_POLICY: {
13
+ readonly operator: {
14
+ readonly vault: false;
15
+ readonly systemdFallback: false;
16
+ readonly attendedFile: true;
17
+ };
18
+ readonly metal: {
19
+ readonly vault: false;
20
+ readonly systemdFallback: true;
21
+ readonly attendedFile: false;
22
+ };
23
+ readonly 'platform-compute': {
24
+ readonly vault: true;
25
+ readonly systemdFallback: true;
26
+ readonly attendedFile: false;
27
+ };
28
+ readonly 'tenant-compute': {
29
+ readonly vault: true;
30
+ readonly systemdFallback: true;
31
+ readonly attendedFile: false;
32
+ };
33
+ };
34
+ export interface DeploymentCredentialBinding {
35
+ /** Exact environment variable requested by a checked-in pipeline step. */
36
+ name: string;
37
+ /** Exact RAM-replica cache key; never a secret value. */
38
+ vaultCacheKey: string;
39
+ /** Same name exposed by systemd, or absent when no fallback was installed. */
40
+ systemdCredential?: string;
41
+ }
42
+ export interface DeploymentCredentialSchema {
43
+ version: 1;
44
+ location: 'platform-compute' | 'tenant-compute';
45
+ projectKey: string;
46
+ environmentKey: string;
47
+ credentials: readonly DeploymentCredentialBinding[];
48
+ }
49
+ /** Build the runtime-only schema from the server-owned enrolment binding. */
50
+ export declare function deploymentCredentialSchema(input: {
51
+ realm: 'platform' | 'tenant';
52
+ projectKey: string;
53
+ environmentKey: string;
54
+ systemdCredentials?: Iterable<string>;
55
+ }): DeploymentCredentialSchema;
56
+ export declare function credentialBinding(schema: DeploymentCredentialSchema, name: string): DeploymentCredentialBinding;
57
+ /** Fixed host credentials are schema entries too, but can never enter a deploy step. */
58
+ export declare const METAL_SYSTEMD_CREDENTIALS: {
59
+ readonly identity: "metal-agent-seed";
60
+ };