@forgezero/agent 0.1.40 → 0.1.42
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/README.md +573 -424
- package/dist/agent-heartbeat.js +6 -3
- package/dist/agent-update-helper.js +5 -2
- package/dist/agent-update.js +5 -2
- package/dist/bootstrap.d.ts +17 -8
- package/dist/bootstrap.js +1504 -512
- package/dist/cli/agent-install.d.ts +6 -5
- package/dist/cli/cloudflare-bootstrap.d.ts +12 -1
- package/dist/cli/maintenance.d.ts +23 -0
- package/dist/cli/run.d.ts +3 -1
- package/dist/cli/session-store.d.ts +5 -0
- package/dist/cloudflare-bootstrap.d.ts +73 -35
- package/dist/cloudflare-bootstrap.js +587 -90
- package/dist/cloudflare-edge.d.ts +64 -12
- package/dist/cloudflare-edge.js +103 -8
- package/dist/community-rehearsal-host.d.ts +51 -0
- package/dist/community-rehearsal-host.js +272 -0
- package/dist/credential-schema.d.ts +54 -0
- package/dist/credential-schema.js +47 -0
- package/dist/definition.d.ts +31 -5
- package/dist/definition.js +271 -44
- package/dist/deploy-file.js +294 -68
- package/dist/deployment-runner.js +18 -5
- package/dist/deployment.d.ts +13 -1
- package/dist/fz-agent.js +9162 -7564
- package/dist/fz-git-ssh.js +122 -0
- package/dist/fz.js +5679 -5077
- package/dist/git-ssh.d.ts +5 -0
- package/dist/guest-enrolment.d.ts +2 -0
- package/dist/guest-enrolment.js +1 -0
- package/dist/host-maintenance.d.ts +39 -0
- package/dist/host-maintenance.js +135 -0
- package/dist/index.d.ts +4 -2
- package/dist/mesh-connector.d.ts +16 -0
- package/dist/mesh-connector.js +46 -0
- package/dist/metal-bootstrap.js +145 -7
- package/dist/metal-helper-socket.js +61 -31
- package/dist/metal-provision.d.ts +2 -2
- package/dist/metal-provision.js +62 -32
- package/dist/operator-bootstrap.d.ts +90 -0
- package/dist/operator-bootstrap.js +5704 -0
- package/dist/otel-collector.d.ts +18 -0
- package/dist/pipeline.d.ts +3 -2
- package/dist/pipeline.js +1 -1
- package/dist/platform-bootstrap-runtime.d.ts +39 -21
- package/dist/platform-bootstrap-runtime.js +182 -59
- package/dist/platform-fleet-verification.d.ts +19 -0
- package/dist/platform-fleet-verification.js +3873 -0
- package/dist/platform-genesis-config.d.ts +7 -0
- package/dist/platform-genesis.d.ts +17 -0
- package/dist/provision.d.ts +76 -3
- package/dist/provision.js +1061 -229
- package/dist/recovery-host.d.ts +7 -0
- package/dist/recovery-host.js +124 -0
- package/dist/service-supervisor.d.ts +42 -0
- package/dist/software-helper.d.ts +4 -0
- package/dist/software-helper.js +865 -63
- package/dist/software.d.ts +14 -3
- package/dist/software.js +163 -37
- package/dist/ssh-bootstrap.d.ts +97 -0
- package/dist/supervised-app.d.ts +2 -0
- package/dist/version.d.ts +1 -1
- package/package.json +175 -164
- package/schema/{deploy-v2.json → deploy-v3.json} +53 -6
|
@@ -4,10 +4,8 @@ export interface CloudflareEdgeConfig {
|
|
|
4
4
|
tunnelId: string;
|
|
5
5
|
hostname: string;
|
|
6
6
|
service: string;
|
|
7
|
+
/** The single attended management capability used for both Tunnel and DNS writes. */
|
|
7
8
|
apiToken: string;
|
|
8
|
-
/** Optional least-privilege overrides. `apiToken` remains the single-token path. */
|
|
9
|
-
tunnelApiToken?: string;
|
|
10
|
-
dnsApiToken?: string;
|
|
11
9
|
}
|
|
12
10
|
export interface CloudflarePrivateRouteConfig {
|
|
13
11
|
accountId: string;
|
|
@@ -73,6 +71,21 @@ interface CloudflareSplitTunnelEntry {
|
|
|
73
71
|
host?: string;
|
|
74
72
|
description?: string;
|
|
75
73
|
}
|
|
74
|
+
/**
|
|
75
|
+
* Reconcile the exact private networks that Mesh nodes must send through
|
|
76
|
+
* Cloudflare. Existing profile entries are preserved; only ForgeZero-owned
|
|
77
|
+
* descriptions are added. The caller must use a dedicated Mesh device profile.
|
|
78
|
+
*/
|
|
79
|
+
export declare function ensureCloudflareWarpNetworkIncludes(config: {
|
|
80
|
+
accountId: string;
|
|
81
|
+
policyId: string;
|
|
82
|
+
networks: readonly string[];
|
|
83
|
+
descriptionPrefix: string;
|
|
84
|
+
apiToken: string;
|
|
85
|
+
}, fetcher?: typeof fetch): Promise<{
|
|
86
|
+
entries: readonly CloudflareSplitTunnelEntry[];
|
|
87
|
+
created: number;
|
|
88
|
+
}>;
|
|
76
89
|
/** Preserve the profile's existing entries while including one exact database host route. */
|
|
77
90
|
export declare function ensureCloudflareWarpDatabaseInclude(config: CloudflareWarpIncludeConfig, fetcher?: typeof fetch): Promise<{
|
|
78
91
|
entries: readonly CloudflareSplitTunnelEntry[];
|
|
@@ -85,21 +98,60 @@ export declare function removeCloudflareWarpDatabaseInclude(config: Omit<Cloudfl
|
|
|
85
98
|
}>;
|
|
86
99
|
/** Reconcile one remotely-managed tunnel route and its proxied DNS record. */
|
|
87
100
|
export declare function configureCloudflareEdge(config: CloudflareEdgeConfig, fetcher?: typeof fetch): Promise<void>;
|
|
88
|
-
export interface CloudflareApiTokens {
|
|
89
|
-
/** Backward-compatible single-token input; two scoped tokens are preferred. */
|
|
90
|
-
apiToken?: string;
|
|
91
|
-
/** Attended management token mapped to Tunnel and DNS operations. */
|
|
92
|
-
tunnelApiToken?: string;
|
|
93
|
-
dnsApiToken?: string;
|
|
94
|
-
/** Runtime token used only for KV node-directory writes. */
|
|
95
|
-
kvApiToken?: string;
|
|
96
|
-
}
|
|
97
101
|
export interface CloudflareTunnel {
|
|
98
102
|
id: string;
|
|
99
103
|
name: string;
|
|
100
104
|
status?: string;
|
|
101
105
|
deleted_at?: string | null;
|
|
102
106
|
}
|
|
107
|
+
export interface CloudflareMeshConnector {
|
|
108
|
+
id: string;
|
|
109
|
+
name: string;
|
|
110
|
+
status?: 'inactive' | 'degraded' | 'healthy' | 'down';
|
|
111
|
+
tun_type?: 'warp_connector';
|
|
112
|
+
deleted_at?: string | null;
|
|
113
|
+
}
|
|
114
|
+
export interface CloudflareDurableObjectNamespace {
|
|
115
|
+
id?: string;
|
|
116
|
+
name?: string;
|
|
117
|
+
class?: string;
|
|
118
|
+
script?: string;
|
|
119
|
+
use_sqlite?: boolean;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Prove that the separately deployed Worker already owns at least one Durable
|
|
123
|
+
* Object namespace. ForgeZero does not deploy or alter the Worker here; this is
|
|
124
|
+
* a read-only control-plane check before installing shared application secrets.
|
|
125
|
+
*/
|
|
126
|
+
export declare function verifyCloudflareWorkerDurableObjects(config: {
|
|
127
|
+
accountId: string;
|
|
128
|
+
scriptName: string;
|
|
129
|
+
apiToken: string;
|
|
130
|
+
}, fetcher?: typeof fetch): Promise<readonly CloudflareDurableObjectNamespace[]>;
|
|
131
|
+
/** Install only the two application secrets consumed by the existing Worker. */
|
|
132
|
+
export declare function configureCloudflareRealtimeSecrets(config: {
|
|
133
|
+
accountId: string;
|
|
134
|
+
scriptName: string;
|
|
135
|
+
publishSecret: string;
|
|
136
|
+
ticketSecret: string;
|
|
137
|
+
apiToken: string;
|
|
138
|
+
}, fetcher?: typeof fetch): Promise<void>;
|
|
139
|
+
/**
|
|
140
|
+
* Create or adopt one Cloudflare Mesh node (the current WARP Connector
|
|
141
|
+
* product name) and retrieve the node-registration token. This is the
|
|
142
|
+
* bidirectional L3/L4 boundary for metal-to-metal traffic; it is deliberately
|
|
143
|
+
* separate from the public, inbound-only cloudflared Tunnel.
|
|
144
|
+
*/
|
|
145
|
+
export declare function ensureCloudflareMeshConnector(config: {
|
|
146
|
+
accountId: string;
|
|
147
|
+
name: string;
|
|
148
|
+
highAvailability: boolean;
|
|
149
|
+
apiToken: string;
|
|
150
|
+
}, fetcher?: typeof fetch): Promise<{
|
|
151
|
+
connector: CloudflareMeshConnector;
|
|
152
|
+
connectorToken: string;
|
|
153
|
+
created: boolean;
|
|
154
|
+
}>;
|
|
103
155
|
/**
|
|
104
156
|
* Create or reuse one remotely-managed Tunnel and obtain its connector token.
|
|
105
157
|
* The token is returned once to the caller so it can be PQ-delivered to the
|
package/dist/cloudflare-edge.js
CHANGED
|
@@ -94,6 +94,39 @@ async function removeCloudflarePrivateDatabaseRoute(config, fetcher = fetch) {
|
|
|
94
94
|
network: privateDatabaseHostRoute(config.privateAddress)
|
|
95
95
|
}, fetcher);
|
|
96
96
|
}
|
|
97
|
+
var privateNetworkCidr = (value) => {
|
|
98
|
+
const [address, prefixText, ...extra] = value.trim().toLowerCase().split("/");
|
|
99
|
+
const family = isIP(address ?? "");
|
|
100
|
+
const prefix = Number(prefixText);
|
|
101
|
+
if (extra.length || !family || !Number.isInteger(prefix) || prefix < 0 || prefix > (family === 4 ? 32 : 128) || !isPrivateDatabaseAddress(address)) {
|
|
102
|
+
throw new Error("Cloudflare WARP include must be an explicit private IPv4 or IPv6 CIDR");
|
|
103
|
+
}
|
|
104
|
+
return `${address}/${prefix}`;
|
|
105
|
+
};
|
|
106
|
+
async function ensureCloudflareWarpNetworkIncludes(config, fetcher = fetch) {
|
|
107
|
+
if (!/^[A-Za-z0-9-]{1,64}$/.test(config.policyId))
|
|
108
|
+
throw new Error("Cloudflare WARP policy id is invalid");
|
|
109
|
+
if (!Array.isArray(config.networks) || config.networks.length < 1 || config.networks.length > 256) {
|
|
110
|
+
throw new Error("Cloudflare WARP networks must contain 1-256 explicit CIDRs");
|
|
111
|
+
}
|
|
112
|
+
const networks = config.networks.map(privateNetworkCidr);
|
|
113
|
+
if (new Set(networks).size !== networks.length)
|
|
114
|
+
throw new Error("Cloudflare WARP networks must be unique");
|
|
115
|
+
const path = `/accounts/${config.accountId}/devices/policy/${config.policyId}/include`;
|
|
116
|
+
const entries = await cf(config, path, {}, fetcher);
|
|
117
|
+
const present = new Set(entries.flatMap(({ address }) => address ? [address.toLowerCase()] : []));
|
|
118
|
+
const additions = networks.filter((network) => !present.has(network)).map((address) => ({
|
|
119
|
+
address,
|
|
120
|
+
description: `${config.descriptionPrefix}:${address}`.slice(0, 100)
|
|
121
|
+
}));
|
|
122
|
+
if (!additions.length)
|
|
123
|
+
return { entries, created: 0 };
|
|
124
|
+
const updated = await cf(config, path, {
|
|
125
|
+
method: "PUT",
|
|
126
|
+
body: JSON.stringify([...entries, ...additions])
|
|
127
|
+
}, fetcher);
|
|
128
|
+
return { entries: updated, created: additions.length };
|
|
129
|
+
}
|
|
97
130
|
async function ensureCloudflareWarpDatabaseInclude(config, fetcher = fetch) {
|
|
98
131
|
if (config.policyId && !/^[A-Za-z0-9-]{1,64}$/.test(config.policyId))
|
|
99
132
|
throw new Error("Cloudflare WARP policy id is invalid");
|
|
@@ -127,13 +160,11 @@ async function removeCloudflareWarpDatabaseInclude(config, fetcher = fetch) {
|
|
|
127
160
|
return { entries: updated, removed: true };
|
|
128
161
|
}
|
|
129
162
|
async function configureCloudflareEdge(config, fetcher = fetch) {
|
|
130
|
-
const tunnelAuth = { apiToken: config.tunnelApiToken?.trim() || config.apiToken };
|
|
131
|
-
const dnsAuth = { apiToken: config.dnsApiToken?.trim() || config.apiToken };
|
|
132
163
|
const tunnelPath = `/accounts/${config.accountId}/cfd_tunnel/${config.tunnelId}/configurations`;
|
|
133
164
|
const dnsPath = `/zones/${config.zoneId}/dns_records`;
|
|
134
165
|
const [current, records] = await Promise.all([
|
|
135
|
-
cf(
|
|
136
|
-
cf(
|
|
166
|
+
cf(config, tunnelPath, {}, fetcher),
|
|
167
|
+
cf(config, `${dnsPath}?name=${encodeURIComponent(config.hostname)}&per_page=1000`, {}, fetcher)
|
|
137
168
|
]);
|
|
138
169
|
if (records.length > 1) {
|
|
139
170
|
throw new Error(`Cloudflare DNS record for ${config.hostname} is ambiguous`);
|
|
@@ -151,7 +182,7 @@ async function configureCloudflareEdge(config, fetcher = fetch) {
|
|
|
151
182
|
...catchAll.length > 0 ? catchAll : [{ service: "http_status:404" }]
|
|
152
183
|
];
|
|
153
184
|
if (JSON.stringify(existing) !== JSON.stringify(desiredIngress)) {
|
|
154
|
-
await cf(
|
|
185
|
+
await cf(config, tunnelPath, {
|
|
155
186
|
method: "PUT",
|
|
156
187
|
body: JSON.stringify({ config: { ingress: desiredIngress } })
|
|
157
188
|
}, fetcher);
|
|
@@ -165,12 +196,72 @@ async function configureCloudflareEdge(config, fetcher = fetch) {
|
|
|
165
196
|
};
|
|
166
197
|
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
198
|
if (!dnsAlreadyCorrect) {
|
|
168
|
-
await cf(
|
|
169
|
-
method: existingRecord ? "
|
|
170
|
-
body: JSON.stringify(
|
|
199
|
+
await cf(config, existingRecord ? `${dnsPath}/${encodeURIComponent(existingRecord.id)}` : dnsPath, {
|
|
200
|
+
method: existingRecord ? "PATCH" : "POST",
|
|
201
|
+
body: JSON.stringify(existingRecord ? {
|
|
202
|
+
...record,
|
|
203
|
+
...existingRecord.comment === undefined ? {} : { comment: existingRecord.comment },
|
|
204
|
+
...existingRecord.tags === undefined ? {} : { tags: existingRecord.tags },
|
|
205
|
+
...existingRecord.settings === undefined ? {} : { settings: existingRecord.settings }
|
|
206
|
+
} : record)
|
|
171
207
|
}, fetcher);
|
|
172
208
|
}
|
|
173
209
|
}
|
|
210
|
+
async function verifyCloudflareWorkerDurableObjects(config, fetcher = fetch) {
|
|
211
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_-]{0,62}$/.test(config.scriptName)) {
|
|
212
|
+
throw new Error("Cloudflare Worker script name is invalid");
|
|
213
|
+
}
|
|
214
|
+
const namespaces = await cf(config, `/accounts/${config.accountId}/workers/durable_objects/namespaces?per_page=1000`, {}, fetcher);
|
|
215
|
+
const owned = namespaces.filter(({ script }) => script === config.scriptName);
|
|
216
|
+
if (!owned.length)
|
|
217
|
+
throw new Error(`Cloudflare Worker ${config.scriptName} has no Durable Object namespace`);
|
|
218
|
+
return owned;
|
|
219
|
+
}
|
|
220
|
+
async function configureCloudflareRealtimeSecrets(config, fetcher = fetch) {
|
|
221
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_-]{0,62}$/.test(config.scriptName) || ![config.publishSecret, config.ticketSecret].every((value) => /^[A-Za-z0-9_-]{64,128}$/.test(value)) || config.publishSecret === config.ticketSecret) {
|
|
222
|
+
throw new Error("Cloudflare realtime secret coordinates are invalid");
|
|
223
|
+
}
|
|
224
|
+
await cf(config, `/accounts/${config.accountId}/workers/scripts/${encodeURIComponent(config.scriptName)}/secrets-bulk`, {
|
|
225
|
+
method: "PATCH",
|
|
226
|
+
body: JSON.stringify({
|
|
227
|
+
secrets: {
|
|
228
|
+
REALTIME_PUBLISH_SECRET: {
|
|
229
|
+
name: "REALTIME_PUBLISH_SECRET",
|
|
230
|
+
text: config.publishSecret,
|
|
231
|
+
type: "secret_text"
|
|
232
|
+
},
|
|
233
|
+
REALTIME_TICKET_SECRET: {
|
|
234
|
+
name: "REALTIME_TICKET_SECRET",
|
|
235
|
+
text: config.ticketSecret,
|
|
236
|
+
type: "secret_text"
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
})
|
|
240
|
+
}, fetcher);
|
|
241
|
+
}
|
|
242
|
+
async function ensureCloudflareMeshConnector(config, fetcher = fetch) {
|
|
243
|
+
const name = config.name.trim();
|
|
244
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/.test(name)) {
|
|
245
|
+
throw new Error("Cloudflare Mesh connector name is invalid");
|
|
246
|
+
}
|
|
247
|
+
const path = `/accounts/${config.accountId}/warp_connector`;
|
|
248
|
+
const connectors = await cf(config, `${path}?is_deleted=false&name=${encodeURIComponent(name)}&per_page=1000`, {}, fetcher);
|
|
249
|
+
const matches = connectors.filter((connector2) => connector2.name === name && !connector2.deleted_at);
|
|
250
|
+
if (matches.length > 1)
|
|
251
|
+
throw new Error(`Cloudflare Mesh connector ${name} is ambiguous`);
|
|
252
|
+
const created = !matches[0];
|
|
253
|
+
const connector = matches[0] ?? await cf(config, path, {
|
|
254
|
+
method: "POST",
|
|
255
|
+
body: JSON.stringify({ name, ha: config.highAvailability })
|
|
256
|
+
}, fetcher);
|
|
257
|
+
if (!connector.id)
|
|
258
|
+
throw new Error("Cloudflare returned an invalid Mesh connector");
|
|
259
|
+
const connectorToken = await cf(config, `${path}/${encodeURIComponent(connector.id)}/token`, {}, fetcher);
|
|
260
|
+
if (!connectorToken || connectorToken.length > 16384) {
|
|
261
|
+
throw new Error("Cloudflare returned an invalid Mesh connector token");
|
|
262
|
+
}
|
|
263
|
+
return { connector, connectorToken, created };
|
|
264
|
+
}
|
|
174
265
|
async function ensureCloudflareTunnel(config, fetcher = fetch) {
|
|
175
266
|
const name = config.name.trim();
|
|
176
267
|
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/.test(name)) {
|
|
@@ -193,12 +284,16 @@ async function ensureCloudflareTunnel(config, fetcher = fetch) {
|
|
|
193
284
|
return { tunnel, connectorToken, created };
|
|
194
285
|
}
|
|
195
286
|
export {
|
|
287
|
+
verifyCloudflareWorkerDurableObjects,
|
|
196
288
|
removeCloudflareWarpDatabaseInclude,
|
|
197
289
|
removeCloudflarePrivateRoute,
|
|
198
290
|
removeCloudflarePrivateDatabaseRoute,
|
|
291
|
+
ensureCloudflareWarpNetworkIncludes,
|
|
199
292
|
ensureCloudflareWarpDatabaseInclude,
|
|
200
293
|
ensureCloudflareTunnel,
|
|
201
294
|
ensureCloudflarePrivateRoute,
|
|
202
295
|
ensureCloudflarePrivateDatabaseRoute,
|
|
296
|
+
ensureCloudflareMeshConnector,
|
|
297
|
+
configureCloudflareRealtimeSecrets,
|
|
203
298
|
configureCloudflareEdge
|
|
204
299
|
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export declare const COMMUNITY_REHEARSAL_VERSION = "3.11.14";
|
|
2
|
+
export declare const COMMUNITY_REHEARSAL_ROOT = "/opt/forgezero-rehearsals/community-cluster-api";
|
|
3
|
+
export declare const COMMUNITY_DATABASE_ROOT = "/var/lib/forgezero-rehearsal-cluster";
|
|
4
|
+
export declare const COMMUNITY_CREDENTIAL_ROOT = "/etc/forgezero-rehearsal/creds";
|
|
5
|
+
export type CommunityRehearsalApiOperation = 'health' | 'init' | 'write' | 'read' | 'query' | 'cluster';
|
|
6
|
+
export type CommunityRehearsalHostRequest = {
|
|
7
|
+
action: 'prepare';
|
|
8
|
+
node: string;
|
|
9
|
+
address: string;
|
|
10
|
+
agency: boolean;
|
|
11
|
+
release: string;
|
|
12
|
+
archivePath: string;
|
|
13
|
+
archiveSha256: string;
|
|
14
|
+
jwt: string;
|
|
15
|
+
} | {
|
|
16
|
+
action: 'database-enable' | 'api-enable' | 'database-ready' | 'starter-ready' | 'api-ready' | 'database-status' | 'api-status';
|
|
17
|
+
node: string;
|
|
18
|
+
} | {
|
|
19
|
+
action: 'api';
|
|
20
|
+
node: string;
|
|
21
|
+
operation: CommunityRehearsalApiOperation;
|
|
22
|
+
key?: string;
|
|
23
|
+
value?: string;
|
|
24
|
+
};
|
|
25
|
+
export type CommunityHostOperation = {
|
|
26
|
+
kind: 'remove-tree';
|
|
27
|
+
path: typeof COMMUNITY_DATABASE_ROOT;
|
|
28
|
+
} | {
|
|
29
|
+
kind: 'write';
|
|
30
|
+
path: string;
|
|
31
|
+
content: string;
|
|
32
|
+
mode: number;
|
|
33
|
+
} | {
|
|
34
|
+
kind: 'unlink';
|
|
35
|
+
path: string;
|
|
36
|
+
} | {
|
|
37
|
+
kind: 'symlink';
|
|
38
|
+
target: string;
|
|
39
|
+
path: string;
|
|
40
|
+
} | {
|
|
41
|
+
kind: 'exec';
|
|
42
|
+
argv: readonly string[];
|
|
43
|
+
stdin?: string;
|
|
44
|
+
accepted?: readonly number[];
|
|
45
|
+
};
|
|
46
|
+
export declare function parseCommunityRehearsalHostRequest(value: unknown): CommunityRehearsalHostRequest;
|
|
47
|
+
export declare function planCommunityRehearsalPrepare(request: Extract<CommunityRehearsalHostRequest, {
|
|
48
|
+
action: 'prepare';
|
|
49
|
+
}>): CommunityHostOperation[];
|
|
50
|
+
/** Execute only the dedicated four-node rehearsal operation selected by a typed request. */
|
|
51
|
+
export declare function runCommunityRehearsalHost(request: CommunityRehearsalHostRequest): Promise<Record<string, unknown>>;
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
// src/community-rehearsal-host.ts
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { lstatSync, mkdirSync, readFileSync, rmSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { dirname } from "node:path";
|
|
5
|
+
var COMMUNITY_REHEARSAL_VERSION = "3.11.14";
|
|
6
|
+
var COMMUNITY_REHEARSAL_ROOT = "/opt/forgezero-rehearsals/community-cluster-api";
|
|
7
|
+
var COMMUNITY_DATABASE_ROOT = "/var/lib/forgezero-rehearsal-cluster";
|
|
8
|
+
var COMMUNITY_CREDENTIAL_ROOT = "/etc/forgezero-rehearsal/creds";
|
|
9
|
+
var NODES = new Map([
|
|
10
|
+
["dev-fz-n1", { address: "10.42.0.21", agency: true }],
|
|
11
|
+
["dev-fz-n2", { address: "10.42.0.22", agency: true }],
|
|
12
|
+
["dev-fz-n3", { address: "10.42.0.23", agency: true }],
|
|
13
|
+
["dev-fz-n4", { address: "10.42.0.24", agency: false }]
|
|
14
|
+
]);
|
|
15
|
+
var RELEASE = /^[a-f0-9]{64}$/;
|
|
16
|
+
var exactObject = (value) => {
|
|
17
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
18
|
+
throw new Error("community rehearsal request must be an object");
|
|
19
|
+
return value;
|
|
20
|
+
};
|
|
21
|
+
var exactNode = (node) => {
|
|
22
|
+
if (typeof node !== "string")
|
|
23
|
+
throw new Error("community rehearsal node is invalid");
|
|
24
|
+
const expected = NODES.get(node);
|
|
25
|
+
if (!expected)
|
|
26
|
+
throw new Error("community rehearsal accepts only the dedicated dev-fz-n1..n4 fleet");
|
|
27
|
+
return { name: node, ...expected };
|
|
28
|
+
};
|
|
29
|
+
function parseCommunityRehearsalHostRequest(value) {
|
|
30
|
+
const row = exactObject(value);
|
|
31
|
+
const node = exactNode(row.node);
|
|
32
|
+
const action = row.action;
|
|
33
|
+
if (action === "prepare") {
|
|
34
|
+
const allowed = ["action", "node", "address", "agency", "release", "archivePath", "archiveSha256", "jwt"];
|
|
35
|
+
if (Object.keys(row).some((key) => !allowed.includes(key)) || row.address !== node.address || row.agency !== node.agency || typeof row.release !== "string" || !RELEASE.test(row.release) || row.archiveSha256 !== row.release || row.archivePath !== `/tmp/forgezero-community-${row.release}.tar.gz` || typeof row.jwt !== "string" || !/^[a-f0-9]{64}$/.test(row.jwt)) {
|
|
36
|
+
throw new Error("community rehearsal prepare request does not match the dedicated fleet contract");
|
|
37
|
+
}
|
|
38
|
+
return row;
|
|
39
|
+
}
|
|
40
|
+
if (["database-enable", "api-enable", "database-ready", "starter-ready", "api-ready", "database-status", "api-status"].includes(String(action))) {
|
|
41
|
+
if (Object.keys(row).some((key) => !["action", "node"].includes(key)))
|
|
42
|
+
throw new Error("community rehearsal host action has unknown fields");
|
|
43
|
+
return { action, node: node.name };
|
|
44
|
+
}
|
|
45
|
+
if (action === "api") {
|
|
46
|
+
if (Object.keys(row).some((key) => !["action", "node", "operation", "key", "value"].includes(key)) || !["health", "init", "write", "read", "query", "cluster"].includes(String(row.operation)) || row.key !== undefined && (typeof row.key !== "string" || !/^[A-Za-z0-9_.:-]{1,128}$/.test(row.key)) || row.value !== undefined && (typeof row.value !== "string" || row.value.length > 4096)) {
|
|
47
|
+
throw new Error("community rehearsal API request is invalid");
|
|
48
|
+
}
|
|
49
|
+
if ((row.operation === "write" || row.operation === "read") && typeof row.key !== "string")
|
|
50
|
+
throw new Error("community rehearsal API key is required");
|
|
51
|
+
if ((row.operation === "write" || row.operation === "query") && typeof row.value !== "string")
|
|
52
|
+
throw new Error("community rehearsal API value is required");
|
|
53
|
+
return row;
|
|
54
|
+
}
|
|
55
|
+
throw new Error("unsupported community rehearsal host action");
|
|
56
|
+
}
|
|
57
|
+
var databaseUnit = (node) => {
|
|
58
|
+
const join = node.name === "dev-fz-n1" ? "" : " --starter.join=10.42.0.21";
|
|
59
|
+
const role = node.agency ? "" : " --cluster.start-agent=false --cluster.start-coordinator=true --cluster.start-dbserver=true";
|
|
60
|
+
return `[Unit]
|
|
61
|
+
Description=ForgeZero isolated Community ${COMMUNITY_REHEARSAL_VERSION} rehearsal cluster
|
|
62
|
+
After=network-online.target
|
|
63
|
+
Wants=network-online.target
|
|
64
|
+
|
|
65
|
+
[Service]
|
|
66
|
+
Type=simple
|
|
67
|
+
User=arangodb
|
|
68
|
+
Group=arangodb
|
|
69
|
+
LoadCredentialEncrypted=arangodb-jwt:${COMMUNITY_CREDENTIAL_ROOT}/arangodb-jwt.cred
|
|
70
|
+
ExecStart=/usr/bin/arangodb --starter.mode=cluster --starter.address=${node.address} --starter.host=${node.address} --starter.data-dir=${COMMUNITY_DATABASE_ROOT} --starter.disable-ipv6 --auth.jwt-secret=%d/arangodb-jwt${join}${role}
|
|
71
|
+
Restart=always
|
|
72
|
+
RestartSec=5
|
|
73
|
+
LimitNOFILE=100000
|
|
74
|
+
LimitCORE=0
|
|
75
|
+
UMask=0077
|
|
76
|
+
NoNewPrivileges=true
|
|
77
|
+
PrivateTmp=true
|
|
78
|
+
PrivateDevices=true
|
|
79
|
+
ProtectSystem=strict
|
|
80
|
+
ProtectHome=true
|
|
81
|
+
ReadWritePaths=${COMMUNITY_DATABASE_ROOT}
|
|
82
|
+
ProtectKernelTunables=true
|
|
83
|
+
ProtectKernelModules=true
|
|
84
|
+
ProtectControlGroups=true
|
|
85
|
+
RestrictSUIDSGID=true
|
|
86
|
+
RestrictRealtime=true
|
|
87
|
+
LockPersonality=true
|
|
88
|
+
|
|
89
|
+
[Install]
|
|
90
|
+
WantedBy=multi-user.target
|
|
91
|
+
`;
|
|
92
|
+
};
|
|
93
|
+
var apiUnit = (node) => `[Unit]
|
|
94
|
+
Description=ForgeZero typed Community cluster rehearsal API
|
|
95
|
+
After=forgezero-rehearsal-db.service network-online.target
|
|
96
|
+
Requires=forgezero-rehearsal-db.service
|
|
97
|
+
|
|
98
|
+
[Service]
|
|
99
|
+
Type=simple
|
|
100
|
+
User=forgezero
|
|
101
|
+
Group=forgezero
|
|
102
|
+
WorkingDirectory=${COMMUNITY_REHEARSAL_ROOT}/current
|
|
103
|
+
LoadCredentialEncrypted=arangodb-jwt:${COMMUNITY_CREDENTIAL_ROOT}/arangodb-jwt.cred
|
|
104
|
+
Environment=HOST=127.0.0.1
|
|
105
|
+
Environment=PORT=8787
|
|
106
|
+
Environment=NODE_NAME=${node.name}
|
|
107
|
+
Environment=ARANGO_URL=http://127.0.0.1:8529
|
|
108
|
+
Environment=ARANGO_URLS=http://10.42.0.21:8529,http://10.42.0.22:8529,http://10.42.0.23:8529,http://10.42.0.24:8529
|
|
109
|
+
Environment=ARANGO_READ_PREFERRED_URLS=http://10.42.0.24:8529
|
|
110
|
+
Environment=ARANGO_READ_PREFERRED_FALLBACK=error
|
|
111
|
+
ExecStart=/usr/local/bin/bun src/index.ts
|
|
112
|
+
Restart=always
|
|
113
|
+
RestartSec=2
|
|
114
|
+
LimitCORE=0
|
|
115
|
+
NoNewPrivileges=true
|
|
116
|
+
PrivateTmp=true
|
|
117
|
+
ProtectSystem=strict
|
|
118
|
+
ProtectHome=true
|
|
119
|
+
ProtectKernelTunables=true
|
|
120
|
+
ProtectKernelModules=true
|
|
121
|
+
ProtectControlGroups=true
|
|
122
|
+
RestrictSUIDSGID=true
|
|
123
|
+
RestrictRealtime=true
|
|
124
|
+
LockPersonality=true
|
|
125
|
+
|
|
126
|
+
[Install]
|
|
127
|
+
WantedBy=multi-user.target
|
|
128
|
+
`;
|
|
129
|
+
function planCommunityRehearsalPrepare(request) {
|
|
130
|
+
const node = exactNode(request.node);
|
|
131
|
+
const releaseRoot = `${COMMUNITY_REHEARSAL_ROOT}/releases/${request.release}`;
|
|
132
|
+
return [
|
|
133
|
+
{ kind: "exec", argv: ["/usr/bin/arangod", "--version"] },
|
|
134
|
+
{ kind: "exec", argv: ["/usr/bin/systemctl", "stop", "forgezero-community-api.service"], accepted: [0, 5] },
|
|
135
|
+
{ kind: "exec", argv: ["/usr/bin/systemctl", "stop", "forgezero-rehearsal-db.service"], accepted: [0, 5] },
|
|
136
|
+
{ kind: "remove-tree", path: COMMUNITY_DATABASE_ROOT },
|
|
137
|
+
{ kind: "exec", argv: ["/usr/bin/install", "-d", "-m", "0700", "-o", "root", "-g", "root", COMMUNITY_CREDENTIAL_ROOT] },
|
|
138
|
+
{ kind: "exec", argv: ["/usr/bin/install", "-d", "-m", "0700", "-o", "arangodb", "-g", "arangodb", COMMUNITY_DATABASE_ROOT] },
|
|
139
|
+
{ kind: "exec", argv: ["/usr/bin/install", "-d", "-m", "0755", "-o", "root", "-g", "root", releaseRoot] },
|
|
140
|
+
{ kind: "unlink", path: `${COMMUNITY_CREDENTIAL_ROOT}/arangodb-jwt.cred.next` },
|
|
141
|
+
{ kind: "exec", argv: ["/usr/bin/systemd-creds", "encrypt", "--name=arangodb-jwt", "-", `${COMMUNITY_CREDENTIAL_ROOT}/arangodb-jwt.cred.next`], stdin: `${request.jwt}
|
|
142
|
+
` },
|
|
143
|
+
{ kind: "exec", argv: ["/usr/bin/chmod", "0400", `${COMMUNITY_CREDENTIAL_ROOT}/arangodb-jwt.cred.next`] },
|
|
144
|
+
{ kind: "exec", argv: ["/usr/bin/mv", "-f", `${COMMUNITY_CREDENTIAL_ROOT}/arangodb-jwt.cred.next`, `${COMMUNITY_CREDENTIAL_ROOT}/arangodb-jwt.cred`] },
|
|
145
|
+
{ kind: "write", path: "/etc/systemd/system/forgezero-rehearsal-db.service", content: databaseUnit(node), mode: 420 },
|
|
146
|
+
{ kind: "exec", argv: ["/usr/bin/tar", "-xzf", request.archivePath, "-C", releaseRoot] },
|
|
147
|
+
{ kind: "exec", argv: ["/usr/bin/chown", "-R", "forgezero:forgezero", releaseRoot] },
|
|
148
|
+
{ kind: "exec", argv: ["/usr/sbin/runuser", "-u", "forgezero", "--", "/usr/local/bin/bun", "install", "--cwd", releaseRoot, "--frozen-lockfile", "--production"] },
|
|
149
|
+
{ kind: "unlink", path: `${COMMUNITY_REHEARSAL_ROOT}/current` },
|
|
150
|
+
{ kind: "symlink", target: releaseRoot, path: `${COMMUNITY_REHEARSAL_ROOT}/current` },
|
|
151
|
+
{ kind: "write", path: "/etc/systemd/system/forgezero-community-api.service", content: apiUnit(node), mode: 420 },
|
|
152
|
+
{ kind: "unlink", path: request.archivePath }
|
|
153
|
+
];
|
|
154
|
+
}
|
|
155
|
+
async function exec(argv, stdin) {
|
|
156
|
+
const child = Bun.spawn([...argv], { stdin: stdin === undefined ? "ignore" : "pipe", stdout: "pipe", stderr: "pipe" });
|
|
157
|
+
if (stdin !== undefined && child.stdin && typeof child.stdin !== "number") {
|
|
158
|
+
child.stdin.write(stdin);
|
|
159
|
+
child.stdin.end();
|
|
160
|
+
}
|
|
161
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
162
|
+
new Response(child.stdout).text(),
|
|
163
|
+
new Response(child.stderr).text(),
|
|
164
|
+
child.exited
|
|
165
|
+
]);
|
|
166
|
+
return { exitCode, output: `${stdout}${stderr}`.slice(0, 256 * 1024) };
|
|
167
|
+
}
|
|
168
|
+
async function applyOperations(operations) {
|
|
169
|
+
for (const operation of operations) {
|
|
170
|
+
if (operation.kind === "remove-tree")
|
|
171
|
+
rmSync(operation.path, { recursive: true, force: true });
|
|
172
|
+
else if (operation.kind === "write") {
|
|
173
|
+
mkdirSync(dirname(operation.path), { recursive: true });
|
|
174
|
+
writeFileSync(operation.path, operation.content, { mode: operation.mode });
|
|
175
|
+
} else if (operation.kind === "unlink") {
|
|
176
|
+
try {
|
|
177
|
+
unlinkSync(operation.path);
|
|
178
|
+
} catch (cause) {
|
|
179
|
+
if (cause.code !== "ENOENT")
|
|
180
|
+
throw cause;
|
|
181
|
+
}
|
|
182
|
+
} else if (operation.kind === "symlink")
|
|
183
|
+
symlinkSync(operation.target, operation.path);
|
|
184
|
+
else {
|
|
185
|
+
const result = await exec(operation.argv, operation.stdin);
|
|
186
|
+
if (!(operation.accepted ?? [0]).includes(result.exitCode))
|
|
187
|
+
throw new Error(`${operation.argv[0]} failed (${result.exitCode}): ${result.output.trim()}`);
|
|
188
|
+
if (operation.argv[0] === "/usr/bin/arangod" && !result.output.split(/\r?\n/, 1)[0]?.includes(COMMUNITY_REHEARSAL_VERSION)) {
|
|
189
|
+
throw new Error(`community rehearsal requires ArangoDB ${COMMUNITY_REHEARSAL_VERSION}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
var responseFor = async (request) => {
|
|
195
|
+
const base = "http://127.0.0.1:8787";
|
|
196
|
+
if (request.operation === "health")
|
|
197
|
+
return fetch(`${base}/health`, { signal: AbortSignal.timeout(15000) });
|
|
198
|
+
if (request.operation === "init")
|
|
199
|
+
return fetch(`${base}/init`, { method: "POST", signal: AbortSignal.timeout(15000) });
|
|
200
|
+
if (request.operation === "write")
|
|
201
|
+
return fetch(`${base}/documents`, {
|
|
202
|
+
method: "POST",
|
|
203
|
+
headers: { "content-type": "application/json" },
|
|
204
|
+
body: JSON.stringify({ key: request.key, value: request.value }),
|
|
205
|
+
signal: AbortSignal.timeout(15000)
|
|
206
|
+
});
|
|
207
|
+
if (request.operation === "read")
|
|
208
|
+
return fetch(`${base}/documents/${encodeURIComponent(request.key)}`, { signal: AbortSignal.timeout(15000) });
|
|
209
|
+
if (request.operation === "query")
|
|
210
|
+
return fetch(`${base}/query?value=${encodeURIComponent(request.value)}`, { signal: AbortSignal.timeout(15000) });
|
|
211
|
+
return fetch(`${base}/cluster`, { signal: AbortSignal.timeout(15000) });
|
|
212
|
+
};
|
|
213
|
+
async function runCommunityRehearsalHost(request) {
|
|
214
|
+
if ((process.getuid?.() ?? -1) !== 0)
|
|
215
|
+
throw new Error("community-rehearsal must run as root");
|
|
216
|
+
const node = exactNode(request.node);
|
|
217
|
+
if (request.action === "prepare") {
|
|
218
|
+
const metadata = lstatSync(request.archivePath);
|
|
219
|
+
if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.size < 1 || metadata.size > 64 * 1024 * 1024 || createHash("sha256").update(readFileSync(request.archivePath)).digest("hex") !== request.archiveSha256) {
|
|
220
|
+
throw new Error("community rehearsal archive is not the declared bounded release");
|
|
221
|
+
}
|
|
222
|
+
await applyOperations(planCommunityRehearsalPrepare(request));
|
|
223
|
+
return { ok: true, action: request.action, node: node.name, release: request.release };
|
|
224
|
+
}
|
|
225
|
+
if (request.action === "database-enable") {
|
|
226
|
+
await applyOperations([
|
|
227
|
+
{ kind: "exec", argv: ["/usr/bin/systemctl", "daemon-reload"] },
|
|
228
|
+
{ kind: "exec", argv: ["/usr/bin/systemctl", "enable", "forgezero-rehearsal-db.service"] },
|
|
229
|
+
{ kind: "exec", argv: ["/usr/bin/systemctl", "restart", "forgezero-rehearsal-db.service"] }
|
|
230
|
+
]);
|
|
231
|
+
return { ok: true, action: request.action, node: node.name };
|
|
232
|
+
}
|
|
233
|
+
if (request.action === "api-enable") {
|
|
234
|
+
await applyOperations([
|
|
235
|
+
{ kind: "exec", argv: ["/usr/bin/systemctl", "daemon-reload"] },
|
|
236
|
+
{ kind: "exec", argv: ["/usr/bin/systemctl", "enable", "--now", "forgezero-community-api.service"] }
|
|
237
|
+
]);
|
|
238
|
+
return { ok: true, action: request.action, node: node.name };
|
|
239
|
+
}
|
|
240
|
+
if (request.action === "database-status" || request.action === "api-status") {
|
|
241
|
+
const unit = request.action === "database-status" ? "forgezero-rehearsal-db.service" : "forgezero-community-api.service";
|
|
242
|
+
const result = await exec(["/usr/bin/systemctl", "is-active", "--quiet", unit]);
|
|
243
|
+
if (result.exitCode !== 0)
|
|
244
|
+
throw new Error(`${unit} is not active`);
|
|
245
|
+
return { ok: true, action: request.action, node: node.name };
|
|
246
|
+
}
|
|
247
|
+
if (request.action === "database-ready" || request.action === "starter-ready" || request.action === "api-ready") {
|
|
248
|
+
const url = request.action === "database-ready" ? "http://127.0.0.1:8529/_api/version" : request.action === "starter-ready" ? "http://10.42.0.21:8528/version" : "http://127.0.0.1:8787/health";
|
|
249
|
+
const response2 = await fetch(url, { signal: AbortSignal.timeout(2000) }).catch(() => {
|
|
250
|
+
return;
|
|
251
|
+
});
|
|
252
|
+
const ready = Boolean(response2 && (request.action === "database-ready" ? response2.status === 200 || response2.status === 401 : response2.ok));
|
|
253
|
+
await response2?.body?.cancel();
|
|
254
|
+
return { ok: true, action: request.action, node: node.name, ready };
|
|
255
|
+
}
|
|
256
|
+
if (request.action !== "api")
|
|
257
|
+
throw new Error("unsupported community rehearsal host action");
|
|
258
|
+
const response = await responseFor(request);
|
|
259
|
+
const body = await response.text();
|
|
260
|
+
if (!response.ok)
|
|
261
|
+
throw new Error(`community rehearsal API returned HTTP ${response.status}: ${body.slice(0, 4096)}`);
|
|
262
|
+
return { ok: true, action: request.action, node: node.name, operation: request.operation, status: response.status, body };
|
|
263
|
+
}
|
|
264
|
+
export {
|
|
265
|
+
runCommunityRehearsalHost,
|
|
266
|
+
planCommunityRehearsalPrepare,
|
|
267
|
+
parseCommunityRehearsalHostRequest,
|
|
268
|
+
COMMUNITY_REHEARSAL_VERSION,
|
|
269
|
+
COMMUNITY_REHEARSAL_ROOT,
|
|
270
|
+
COMMUNITY_DATABASE_ROOT,
|
|
271
|
+
COMMUNITY_CREDENTIAL_ROOT
|
|
272
|
+
};
|
|
@@ -9,6 +9,60 @@
|
|
|
9
9
|
*/
|
|
10
10
|
export declare const AGENT_CREDENTIAL_LOCATIONS: readonly ["operator", "metal", "platform-compute", "tenant-compute"];
|
|
11
11
|
export type AgentCredentialLocation = (typeof AGENT_CREDENTIAL_LOCATIONS)[number];
|
|
12
|
+
/**
|
|
13
|
+
* Stable logical names shared by operator input, Vault fields and systemd.
|
|
14
|
+
*
|
|
15
|
+
* `CF_TUNNEL_TOKEN` is the broad attended/tenant-control credential that may
|
|
16
|
+
* create Tunnels and reconcile one exact DNS hostname. It is attended input
|
|
17
|
+
* before custody and Vault-only for the post-operational platform controller;
|
|
18
|
+
* it is never loaded by cloudflared. cloudflared receives only the connector
|
|
19
|
+
* returned for its own Tunnel.
|
|
20
|
+
*
|
|
21
|
+
* `CF_API_TOKEN` is the exact-account runtime/control credential used for
|
|
22
|
+
* Workers KV and the existing Worker's secret bindings. Durable Object
|
|
23
|
+
* application messages still go through that Worker's binding and use the
|
|
24
|
+
* separate REALTIME_PUBLISH_SECRET; a Cloudflare REST token is not a Durable
|
|
25
|
+
* Object invocation credential.
|
|
26
|
+
*/
|
|
27
|
+
export declare const CLOUDFLARE_CREDENTIAL_NAMES: {
|
|
28
|
+
readonly api: "CF_API_TOKEN";
|
|
29
|
+
readonly tunnel: "CF_TUNNEL_TOKEN";
|
|
30
|
+
readonly connector: "CF_TUNNEL_CONNECTOR_TOKEN";
|
|
31
|
+
readonly realtimePublish: "REALTIME_PUBLISH_SECRET";
|
|
32
|
+
readonly realtimeTicket: "REALTIME_TICKET_SECRET";
|
|
33
|
+
};
|
|
34
|
+
export declare const CLOUDFLARE_CREDENTIAL_SCHEMA: {
|
|
35
|
+
readonly CF_TUNNEL_TOKEN: {
|
|
36
|
+
readonly permissions: readonly ["Account:Cloudflare Tunnel Write", "Account:Cloudflare One Connector: WARP Write", "Account:Cloudflare One Networks Write", "Account:Zero Trust Write", "Zone:DNS Write"];
|
|
37
|
+
readonly platformBootstrap: "attended-file";
|
|
38
|
+
readonly platformRuntime: "vault";
|
|
39
|
+
readonly tenantControl: "vault";
|
|
40
|
+
};
|
|
41
|
+
readonly CF_API_TOKEN: {
|
|
42
|
+
readonly permissions: readonly ["Account:Workers KV Storage Write", "Account:Workers Scripts Write"];
|
|
43
|
+
readonly platformBootstrap: "attended-file";
|
|
44
|
+
readonly platformRuntime: "vault-then-systemd";
|
|
45
|
+
readonly tenantControl: "vault";
|
|
46
|
+
};
|
|
47
|
+
readonly CF_TUNNEL_CONNECTOR_TOKEN: {
|
|
48
|
+
readonly permissions: readonly ["one named Tunnel connector"];
|
|
49
|
+
readonly platformBootstrap: "derived";
|
|
50
|
+
readonly platformRuntime: "systemd";
|
|
51
|
+
readonly tenantControl: "derived";
|
|
52
|
+
};
|
|
53
|
+
readonly REALTIME_PUBLISH_SECRET: {
|
|
54
|
+
readonly permissions: readonly ["Worker realtime publish endpoint"];
|
|
55
|
+
readonly platformBootstrap: "generated-and-installed-worker-secret";
|
|
56
|
+
readonly platformRuntime: "vault-then-systemd";
|
|
57
|
+
readonly tenantControl: "vault";
|
|
58
|
+
};
|
|
59
|
+
readonly REALTIME_TICKET_SECRET: {
|
|
60
|
+
readonly permissions: readonly ["Worker realtime subscription tickets"];
|
|
61
|
+
readonly platformBootstrap: "generated-and-installed-worker-secret";
|
|
62
|
+
readonly platformRuntime: "vault-then-systemd";
|
|
63
|
+
readonly tenantControl: "vault";
|
|
64
|
+
};
|
|
65
|
+
};
|
|
12
66
|
export declare const AGENT_CREDENTIAL_POLICY: {
|
|
13
67
|
readonly operator: {
|
|
14
68
|
readonly vault: false;
|