@fabrika/provider-zerops 0.8.2 → 0.9.0-cloudflare.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.
Files changed (128) hide show
  1. package/package.json +16 -14
  2. package/src/api/client.ts +6 -0
  3. package/src/api/types.ts +12 -0
  4. package/src/cli.ts +11 -0
  5. package/src/collaborators.ts +0 -14
  6. package/src/control.ts +33 -57
  7. package/src/execution.ts +113 -102
  8. package/src/installation/admin.ts +1 -1
  9. package/src/installation/deploy-options.ts +1 -2
  10. package/src/installation/deploy.ts +41 -75
  11. package/src/installation/generated/platform-console-schema.ts +3 -0
  12. package/src/installation/generated/platform-proxy-manifest.ts +21 -1
  13. package/src/installation/guide.md +9 -6
  14. package/src/installation/hosts.ts +1 -7
  15. package/src/installation/index.ts +0 -13
  16. package/src/installation/init-options.ts +2 -1
  17. package/src/installation/install-environment.ts +42 -106
  18. package/src/installation/install-options.ts +3 -1
  19. package/src/installation/install-secrets.ts +24 -68
  20. package/src/installation/install.ts +8 -4
  21. package/src/installation/sidecar.ts +5 -26
  22. package/src/installation/topology/setups.ts +5 -0
  23. package/src/installation/upgrade-options.ts +2 -2
  24. package/src/managed-env.ts +1 -174
  25. package/src/manifest.ts +11 -14
  26. package/src/namespace/claims.ts +56 -1
  27. package/src/namespace/errors.ts +1 -1
  28. package/src/namespace/manifest.ts +6 -19
  29. package/src/namespace/provisioning.ts +15 -3
  30. package/src/namespace/proxy-attempt.ts +43 -2
  31. package/src/namespace/proxy-reads.ts +12 -22
  32. package/src/namespace-cli.ts +152 -0
  33. package/src/plan.ts +2 -7
  34. package/src/provider.ts +128 -126
  35. package/src/proxy-manifest-carrier.ts +18 -0
  36. package/src/types.ts +0 -2
  37. package/types/src/api/client.d.ts +5 -0
  38. package/types/src/api/errors.d.ts +24 -0
  39. package/types/src/api/polling.d.ts +41 -0
  40. package/types/src/api/readers.d.ts +1115 -0
  41. package/types/src/api/service-env-limit.d.ts +17 -0
  42. package/types/src/api/service-env-response.d.ts +23 -0
  43. package/types/src/api/types.d.ts +687 -0
  44. package/types/src/api.d.ts +6 -0
  45. package/types/src/authoring.d.ts +9 -0
  46. package/types/src/cli-args.d.ts +19 -0
  47. package/types/src/cli.d.ts +13 -0
  48. package/types/src/collaborators.d.ts +16 -0
  49. package/types/src/compile.d.ts +91 -0
  50. package/types/src/control.d.ts +40 -0
  51. package/types/src/database-budget.d.ts +38 -0
  52. package/types/src/descriptor.d.ts +28 -0
  53. package/types/src/execution.d.ts +125 -0
  54. package/types/src/exposure.d.ts +47 -0
  55. package/types/src/index.d.ts +31 -0
  56. package/types/src/installation/admin-options.d.ts +24 -0
  57. package/types/src/installation/admin.d.ts +35 -0
  58. package/types/src/installation/checker-release.d.ts +12 -0
  59. package/types/src/installation/deploy-options.d.ts +37 -0
  60. package/types/src/installation/deploy.d.ts +136 -0
  61. package/types/src/installation/generated/platform-console-schema.d.ts +2 -0
  62. package/types/src/installation/generated/platform-proxy-manifest.d.ts +2 -0
  63. package/types/src/installation/hosts.d.ts +34 -0
  64. package/types/src/installation/index.d.ts +7 -0
  65. package/types/src/installation/init-options.d.ts +38 -0
  66. package/types/src/installation/init.d.ts +60 -0
  67. package/types/src/installation/install-collaborators.d.ts +24 -0
  68. package/types/src/installation/install-environment.d.ts +63 -0
  69. package/types/src/installation/install-options.d.ts +49 -0
  70. package/types/src/installation/install-project.d.ts +13 -0
  71. package/types/src/installation/install-provisioning.d.ts +58 -0
  72. package/types/src/installation/install-secrets.d.ts +17 -0
  73. package/types/src/installation/install-token.d.ts +10 -0
  74. package/types/src/installation/install.d.ts +23 -0
  75. package/types/src/installation/installation-source.d.ts +8 -0
  76. package/types/src/installation/log.d.ts +44 -0
  77. package/types/src/installation/machine-key.d.ts +19 -0
  78. package/types/src/installation/placement-options.d.ts +11 -0
  79. package/types/src/installation/platform-archive.d.ts +7 -0
  80. package/types/src/installation/platform-source.d.ts +8 -0
  81. package/types/src/installation/platform-upload.d.ts +3 -0
  82. package/types/src/installation/sidecar.d.ts +49 -0
  83. package/types/src/installation/source-options.d.ts +1 -0
  84. package/types/src/installation/topology/artifacts.d.ts +8 -0
  85. package/types/src/installation/topology/index.d.ts +5 -0
  86. package/types/src/installation/topology/installation-schemas.d.ts +2242 -0
  87. package/types/src/installation/topology/invariants.d.ts +23 -0
  88. package/types/src/installation/topology/setups.d.ts +59 -0
  89. package/types/src/installation/topology/topology.d.ts +153 -0
  90. package/types/src/installation/topology/validate.d.ts +11 -0
  91. package/types/src/installation/upgrade-options.d.ts +26 -0
  92. package/types/src/installation/upgrade-output.d.ts +6 -0
  93. package/types/src/installation/upgrade.d.ts +88 -0
  94. package/types/src/managed-env.d.ts +24 -0
  95. package/types/src/manifest.d.ts +204 -0
  96. package/types/src/namespace/claims.d.ts +16 -0
  97. package/types/src/namespace/codec.d.ts +334 -0
  98. package/types/src/namespace/constants.d.ts +25 -0
  99. package/types/src/namespace/errors.d.ts +15 -0
  100. package/types/src/namespace/manifest.d.ts +4 -0
  101. package/types/src/namespace/operator.d.ts +12 -0
  102. package/types/src/namespace/provisioning.d.ts +4 -0
  103. package/types/src/namespace/proxy-attempt.d.ts +39 -0
  104. package/types/src/namespace/proxy-reads.d.ts +22 -0
  105. package/types/src/namespace/proxy-secrets.d.ts +8 -0
  106. package/types/src/namespace/proxy-source.d.ts +74 -0
  107. package/types/src/namespace/topology.d.ts +9 -0
  108. package/types/src/namespace/types.d.ts +101 -0
  109. package/types/src/namespace/update.d.ts +6 -0
  110. package/types/src/namespace/validation.d.ts +12 -0
  111. package/types/src/namespace-cli.d.ts +2 -0
  112. package/types/src/namespace.d.ts +9 -0
  113. package/types/src/plan.d.ts +11 -0
  114. package/types/src/provider.d.ts +63 -0
  115. package/types/src/proxy-manifest-carrier.d.ts +24 -0
  116. package/types/src/schema.generated.d.ts +376 -0
  117. package/types/src/service-names.d.ts +4 -0
  118. package/types/src/service-references.d.ts +13 -0
  119. package/types/src/source/constants.d.ts +6 -0
  120. package/types/src/source/port.d.ts +60 -0
  121. package/types/src/source/types.d.ts +4 -0
  122. package/types/src/source/validation.d.ts +19 -0
  123. package/types/src/source.d.ts +2 -0
  124. package/types/src/types.d.ts +119 -0
  125. package/types/src/version-attempt.d.ts +8 -0
  126. package/src/installation/mcp.ts +0 -102
  127. package/src/installation/proxy-manifest.ts +0 -214
  128. package/src/installation/secrets.ts +0 -78
@@ -6,6 +6,7 @@
6
6
  // so a pipeline or an agent drives init end to end. Its confirmations stay, answered by `--yes`.
7
7
 
8
8
  import {
9
+ assertPinnedTag,
9
10
  CHECKOUT_FLAG,
10
11
  type CommandEnv,
11
12
  describeArgument,
@@ -17,7 +18,7 @@ import {
17
18
  UNATTENDED_FLAG,
18
19
  } from '@fabrika/installation-init'
19
20
  import { parsePlatformHosts } from './placement-options'
20
- import { assertPinnedTag, defaultCheckoutName, defaultSidecarRepo } from './sidecar'
21
+ import { defaultCheckoutName, defaultSidecarRepo } from './sidecar'
21
22
 
22
23
  /** The name every bypass in this platform is gated on, and therefore the one name an installation cannot take. */
23
24
  const LOCAL_ENVIRONMENT = 'local'
@@ -1,11 +1,17 @@
1
- import type { InstallationEmail } from '@fabrika/installation-init'
1
+ import {
2
+ INSTALLATION_SECRET_VARIABLES,
3
+ type InstallationEmail,
4
+ type InstallationSecrets,
5
+ installationServiceEnvironment,
6
+ type PlatformHosts,
7
+ platformOrigin,
8
+ type PlatformStorage,
9
+ } from '@fabrika/installation-init'
2
10
  import { encodeProxyManifestJson } from '@fabrika/proxy-core/manifest'
3
11
  import type { ZeropsNamespaceProxySource } from '../index'
4
12
  import type { PlatformDeployService, ResolvedService } from './deploy'
5
- import { type PlatformHosts, platformOrigin } from './hosts'
6
13
  import { assertRunning, type InstallCollaborators } from './install-collaborators'
7
14
  import { FABRIKA_ZEROPS_PROXY_SOURCE_JSON, serializeNamespaceProxySource } from './installation-source'
8
- import type { InstallationSecrets } from './secrets'
9
15
  import type { PlatformTier } from './topology/topology'
10
16
 
11
17
  /** The private address the proxy dials IAM at inside the project, and pass 1's placeholder issuer. */
@@ -16,14 +22,7 @@ const PRIVATE_IAM_URL = `http://${PRIVATE_IAM_UPSTREAM}`
16
22
  const postgresUrl = (service: string): string => `\${${service}_connectionTlsString}/\${${service}_dbName}?sslmode=require`
17
23
 
18
24
  /** An object-storage service's coordinates, as the platform's own `${service_variable}` references. */
19
- interface StorageReferences {
20
- readonly bucket: string
21
- readonly accessKeyId: string
22
- readonly secretAccessKey: string
23
- readonly endpoint: string
24
- }
25
-
26
- const storageOf = (service: string): StorageReferences => ({
25
+ const storageOf = (service: string): PlatformStorage => ({
27
26
  bucket: `\${${service}_bucketName}`,
28
27
  accessKeyId: `\${${service}_accessKeyId}`,
29
28
  secretAccessKey: `\${${service}_secretAccessKey}`,
@@ -72,51 +71,40 @@ export interface InstallationEnvInput {
72
71
  }
73
72
 
74
73
  /**
75
- * EVERY variable this command places, per service.
74
+ * EVERY variable this command places, per service: fabrika's (`installationServiceEnvironment`) over this
75
+ * project's own references, and control's Zerops variables after them.
76
76
  *
77
77
  * Pure, and exported, because the matrix is the thing most worth pinning: a name that is right on
78
- * Cloudflare and wrong on Bun is a silent no-op rather than a failure. Two spelling rules run through
79
- * it, both verified against the runtimes rather than assumed:
80
- *
81
- * • IAM's Bun runtime reads `ISSUER` and `ENVIRONMENT` UNPREFIXED and everything else with the
82
- * `FABRIKA_IAM_` prefix (`packages/iam/src/node/runtime.ts`). The prefixed spellings below are the
83
- * ones that file actually reads; the unprefixed ones are the ones it actually reads too.
84
- * • No key any service declares in its own `zerops.yaml` appears here — `PORT`,
85
- * `FABRIKA_IAM_RPC_URL`, `FABRIKA_OPERATIONS_URL`, `FABRIKA_CONTROL_ASSETS_DIR`,
86
- * `FABRIKA_CONTROL_URL`, `FABRIKA_PROXY_MANIFEST`, `FABRIKA_PROXY_PORT`. The env API refuses those
87
- * (`provider-zerops/src/api.ts`), because the conflict cannot be resolved to a record id.
88
- *
89
- * The proxy manifest is deliberately absent: pass 1 writes an empty one and `deployPlatform` owns the
90
- * real one, in the same run as the code it describes (ADR-0027).
78
+ * Cloudflare and wrong on Bun is a silent no-op rather than a failure. No key any service declares in its
79
+ * own `zerops.yaml` appears here — `PORT`, `FABRIKA_IAM_RPC_URL`, `FABRIKA_OPERATIONS_URL`,
80
+ * `FABRIKA_CONTROL_ASSETS_DIR`, `FABRIKA_CONTROL_URL`, `FABRIKA_PROXY_MANIFEST`, `FABRIKA_PROXY_PORT`. The env
81
+ * API refuses those (`provider-zerops/src/api.ts`), because the conflict cannot be resolved to a record id.
91
82
  */
92
83
  export const installationServiceEnv = (input: InstallationEnvInput): ReadonlyMap<PlatformDeployService, ReadonlyMap<string, string>> => {
93
84
  const { secrets } = input
94
- const iamOrigin = platformOrigin(input.scheme, input.hosts.iam)
95
- const operationsOrigin = platformOrigin(input.scheme, input.hosts.operations)
96
- const platformDatabase = postgresUrl('db')
97
- const runLogs = storageOf('storage')
98
85
  const operationsData = OPERATIONS_DATA[input.tier]
99
- const operationsBlobs = storageOf(operationsData.storage)
86
+ const fabrika = installationServiceEnvironment({
87
+ environment: input.environment,
88
+ scheme: input.scheme,
89
+ hosts: input.hosts,
90
+ iamUpstream: PRIVATE_IAM_UPSTREAM,
91
+ secrets,
92
+ email: input.email,
93
+ data: {
94
+ iamDatabase: postgresUrl('db'),
95
+ controlDatabase: postgresUrl('db'),
96
+ operationsDatabase: postgresUrl(operationsData.database),
97
+ runLogs: storageOf('storage'),
98
+ operationsBlobs: storageOf(operationsData.storage),
99
+ },
100
+ })
100
101
  const control = new Map([
101
- ['FABRIKA_CONTROL_DATABASE_URL', platformDatabase],
102
- ['ENVIRONMENT', input.environment],
103
- // A bare HOST, not an origin: `controlPublicOrigin` accepts either.
104
- ['FABRIKA_CONTROL_DOMAIN', input.hosts.control],
105
- ['FABRIKA_IAM_ISSUER', iamOrigin],
106
- ['OPERATIONS_ARTIFACT_ORIGIN', operationsOrigin],
107
- ['FABRIKA_CONTROL_RUN_LOGS_BUCKET', runLogs.bucket],
108
- ['FABRIKA_CONTROL_RUN_LOGS_ACCESS_KEY_ID', runLogs.accessKeyId],
109
- ['FABRIKA_CONTROL_RUN_LOGS_SECRET_ACCESS_KEY', runLogs.secretAccessKey],
110
- ['FABRIKA_CONTROL_RUN_LOGS_ENDPOINT', runLogs.endpoint],
111
- ['FABRIKA_IAM_RPC_KEY', secrets.rpcKey],
112
- ['OPERATIONS_SYNC_KEY', secrets.operationsSyncKey],
113
- ['FABRIKA_CONTROL_VAULT_KEY', secrets.vaultKey],
114
- ['FABRIKA_IAM_PROVISIONING_KEY', secrets.provisioningKey],
102
+ ...fabrika.control,
115
103
  ['FABRIKA_ZEROPS_CLIENT_ID', input.clientId],
116
104
  [FABRIKA_ZEROPS_PROXY_SOURCE_JSON, serializeNamespaceProxySource(input.proxySource)],
117
105
  // The PUBLIC IAM origin an application project's proxy reaches: there is no private network
118
106
  // between Zerops projects (ADR-0006).
119
- ['FABRIKA_ZEROPS_PROXY_IAM_URL', iamOrigin],
107
+ ['FABRIKA_ZEROPS_PROXY_IAM_URL', platformOrigin(input.scheme, input.hosts.iam)],
120
108
  ['FABRIKA_ZEROPS_PROXY_IAM_KEY', secrets.proxyKey],
121
109
  ['FABRIKA_ZEROPS_ACCESS_TOKEN', input.zeropsAccessToken],
122
110
  ['FABRIKA_ZEROPS_PLATFORM_PROJECT_ID', input.projectId],
@@ -126,79 +114,27 @@ export const installationServiceEnv = (input: InstallationEnvInput): ReadonlyMap
126
114
  // the CLI reads `FABRIKA_ZEROPS_API_URL`; they are different processes reading different sources.
127
115
  control.set('FABRIKA_ZEROPS_API_BASE_URL', input.apiBaseUrl)
128
116
  }
129
- const email = input.email.kind === 'none' ? [] : [...input.email.variables]
130
- for (const [key, value] of email) control.set(key, value)
131
117
  return new Map<PlatformDeployService, ReadonlyMap<string, string>>([
132
- [
133
- 'iam',
134
- new Map([
135
- ['FABRIKA_IAM_DATABASE_URL', platformDatabase],
136
- ['ENVIRONMENT', input.environment],
137
- ['ISSUER', iamOrigin],
138
- // Password only, by decision: OIDC needs an identity provider the operator has not been
139
- // asked for. Both are STATED because IAM's own defaults are the other way round — OIDC on,
140
- // password off — and it refuses to boot with neither.
141
- ['FABRIKA_IAM_OIDC_ENABLED', 'false'],
142
- ['FABRIKA_IAM_PASSWORD_ENABLED', 'true'],
143
- ['FABRIKA_IAM_SIGNING_KEYS', secrets.signingKeys],
144
- ['FABRIKA_IAM_RPC_KEY', secrets.rpcKey],
145
- ['FABRIKA_IAM_PROXY_KEY', secrets.proxyKey],
146
- ['FABRIKA_IAM_PROVISIONING_KEY', secrets.provisioningKey],
147
- ...email,
148
- ]),
149
- ],
150
- [
151
- 'operations',
152
- new Map([
153
- ['FABRIKA_OPERATIONS_DATABASE_URL', postgresUrl(operationsData.database)],
154
- ['FABRIKA_OPERATIONS_BLOB_BUCKET', operationsBlobs.bucket],
155
- ['FABRIKA_OPERATIONS_BLOB_ACCESS_KEY_ID', operationsBlobs.accessKeyId],
156
- ['FABRIKA_OPERATIONS_BLOB_SECRET_ACCESS_KEY', operationsBlobs.secretAccessKey],
157
- ['FABRIKA_OPERATIONS_BLOB_ENDPOINT', operationsBlobs.endpoint],
158
- ['FABRIKA_OPERATIONS_PUBLIC_HOST', input.hosts.operations],
159
- ['ENVIRONMENT', input.environment],
160
- ['FABRIKA_IAM_ISSUER', iamOrigin],
161
- ['OPERATIONS_SYNC_KEY', secrets.operationsSyncKey],
162
- ['FABRIKA_IAM_RPC_KEY', secrets.rpcKey],
163
- ]),
164
- ],
118
+ ['iam', fabrika.iam],
119
+ ['operations', fabrika.operations],
165
120
  ['control', control],
166
- [
167
- 'proxy',
168
- new Map([
169
- ['ENVIRONMENT', input.environment],
170
- // Compared byte-for-byte against a token's `iss`, so this is the PUBLIC issuer and not the
171
- // private `iam:3000` pass 1 wrote.
172
- ['FABRIKA_IAM_ISSUER', iamOrigin],
173
- ['FABRIKA_IAM_KEY', secrets.proxyKey],
174
- ['FABRIKA_PROXY_SESSION_OWNER_JSON', JSON.stringify({ issuer: iamOrigin, upstream: PRIVATE_IAM_UPSTREAM })],
175
- ]),
176
- ],
121
+ ['proxy', fabrika.proxy],
177
122
  ])
178
123
  }
179
124
 
180
125
  /**
181
- * The keys whose VALUE this command generates, per service.
126
+ * The keys whose VALUE this command generates, per service: fabrika's, and the two Zerops names on control
127
+ * that carry the proxy key and the minted token.
182
128
  *
183
129
  * Their presence on a live service is what "this project already holds an installation" means, and it is
184
130
  * checked by KEY only — a value is never read back for comparison. `__tests__/install.test.ts` pins this
185
131
  * against the matrix above, so a new generated secret cannot be added without appearing here.
186
132
  */
187
133
  export const GENERATED_SECRET_KEYS: ReadonlyMap<PlatformDeployService, readonly string[]> = new Map<PlatformDeployService, readonly string[]>([
188
- ['iam', ['FABRIKA_IAM_SIGNING_KEYS', 'FABRIKA_IAM_RPC_KEY', 'FABRIKA_IAM_PROXY_KEY', 'FABRIKA_IAM_PROVISIONING_KEY']],
189
- ['operations', ['OPERATIONS_SYNC_KEY', 'FABRIKA_IAM_RPC_KEY']],
190
- [
191
- 'control',
192
- [
193
- 'FABRIKA_IAM_RPC_KEY',
194
- 'OPERATIONS_SYNC_KEY',
195
- 'FABRIKA_CONTROL_VAULT_KEY',
196
- 'FABRIKA_IAM_PROVISIONING_KEY',
197
- 'FABRIKA_ZEROPS_PROXY_IAM_KEY',
198
- 'FABRIKA_ZEROPS_ACCESS_TOKEN',
199
- ],
200
- ],
201
- ['proxy', ['FABRIKA_IAM_KEY']],
134
+ ['iam', INSTALLATION_SECRET_VARIABLES.iam],
135
+ ['operations', INSTALLATION_SECRET_VARIABLES.operations],
136
+ ['control', [...INSTALLATION_SECRET_VARIABLES.control, 'FABRIKA_ZEROPS_PROXY_IAM_KEY', 'FABRIKA_ZEROPS_ACCESS_TOKEN']],
137
+ ['proxy', INSTALLATION_SECRET_VARIABLES.proxy],
202
138
  ])
203
139
 
204
140
  /**
@@ -10,6 +10,7 @@
10
10
 
11
11
  import { builtinEmailProviders } from '@fabrika/email/providers'
12
12
  import {
13
+ checkedSecretsFilePath,
13
14
  describeArgument,
14
15
  EMAIL_FLAGS,
15
16
  type InstallationEmail,
@@ -17,10 +18,11 @@ import {
17
18
  readFlag,
18
19
  readInstallationEmail,
19
20
  requiredSetting,
21
+ SECRETS_FILE_FLAG,
22
+ SECRETS_FILE_VARIABLE,
20
23
  setting,
21
24
  UNATTENDED_FLAG,
22
25
  } from '@fabrika/installation-init'
23
- import { checkedSecretsFilePath, SECRETS_FILE_FLAG, SECRETS_FILE_VARIABLE } from './install-secrets'
24
26
  import { parsePlatformScheme } from './placement-options'
25
27
  import { readInstallationSourceDir } from './source-options'
26
28
  import type { PlatformTier } from './topology/topology'
@@ -1,36 +1,10 @@
1
- // The local copy of every credential `platform install` creates.
1
+ // What a Zerops installation's secrets file records.
2
2
  //
3
3
  // The services hold the same values in Zerops, but stored SENSITIVE, and Zerops hands a sensitive value
4
- // back only to a token in sudo mode (measured 2026-10-15). So the operator's copy is the one this
5
- // command writes, before the first of them is placed: a run that fails later loses nothing.
4
+ // back only to a token in sudo mode (measured 2026-10-15). The file's path rule, quoting and 0600 write
5
+ // are `@fabrika/installation-init`'s `secrets-file.ts`.
6
6
 
7
- import { existsSync } from 'node:fs'
8
- import { open } from 'node:fs/promises'
9
- import { isAbsolute, relative, resolve } from 'node:path'
10
- import type { InstallationSecrets } from './secrets'
11
-
12
- /** `--secrets-file`: where the local copy goes. A new file, outside the source checkout. */
13
- export const SECRETS_FILE_FLAG = '--secrets-file'
14
- export const SECRETS_FILE_VARIABLE = 'FABRIKA_PLATFORM_SECRETS_FILE'
15
-
16
- /**
17
- * Settle the path before anything is contacted.
18
- *
19
- * Refused when it exists, because overwriting one would destroy another installation's only copy, and
20
- * when it is inside the source checkout, where it would dirty the checkout `platform deploy` uploads
21
- * and could be committed.
22
- */
23
- export const checkedSecretsFilePath = (path: string, sourceDir: string): string => {
24
- const absolute = resolve(path)
25
- const fromSource = relative(resolve(sourceDir), absolute)
26
- if (fromSource === '' || (!fromSource.startsWith('..') && !isAbsolute(fromSource))) {
27
- throw new Error(`${SECRETS_FILE_FLAG}=${path} is inside the source checkout — name a path outside it, so it is never uploaded or committed`)
28
- }
29
- if (existsSync(absolute)) {
30
- throw new Error(`${SECRETS_FILE_FLAG}=${path} already exists — it may be another installation's only copy; name a new file`)
31
- }
32
- return absolute
33
- }
7
+ import { type InstallationSecrets, renderSecretsFile } from '@fabrika/installation-init'
34
8
 
35
9
  /** What the file records beside the credentials, so it identifies its installation on its own. */
36
10
  export interface InstallationSecretsRecord {
@@ -43,47 +17,29 @@ export interface InstallationSecretsRecord {
43
17
  }
44
18
 
45
19
  /**
46
- * The file's text: shell-sourceable assignments, named as the commands that read them name them.
20
+ * The file's text, named as the commands that read it name each value.
47
21
  *
48
22
  * `FABRIKA_ZEROPS_ACCESS_TOKEN` is the INTEGRATION token, which is what `platform init` and
49
23
  * `platform deploy` take; the personal token the install ran with is not written.
50
24
  */
51
- export const renderSecretsFile = (record: InstallationSecretsRecord): string => {
25
+ export const renderInstallationSecretsFile = (record: InstallationSecretsRecord): string => {
52
26
  const { secrets } = record
53
- const assignments: readonly (readonly [string, string])[] = [
54
- ['FABRIKA_ZEROPS_PROJECT_ID', record.projectId],
55
- ['FABRIKA_ZEROPS_CLIENT_ID', record.clientId],
56
- ['FABRIKA_IAM_PROVISIONING_KEY', secrets.provisioningKey],
57
- ['FABRIKA_ZEROPS_ACCESS_TOKEN', record.zeropsAccessToken],
58
- ['FABRIKA_IAM_RPC_KEY', secrets.rpcKey],
59
- ['FABRIKA_IAM_PROXY_KEY', secrets.proxyKey],
60
- ['OPERATIONS_SYNC_KEY', secrets.operationsSyncKey],
61
- ['FABRIKA_CONTROL_VAULT_KEY', secrets.vaultKey],
62
- ['FABRIKA_IAM_SIGNING_KEYS', secrets.signingKeys],
63
- ]
64
- const lines = assignments.map(([name, value]) => {
65
- // Single quotes keep every character literal; none of these values can contain one.
66
- if (value.includes("'") || value.includes('\n')) {
67
- throw new Error(`${name} cannot be written to the secrets file as one quoted line`)
68
- }
69
- return `${name}='${value}'`
70
- })
71
- return [
72
- `# fabrika on Zerops — installation \`${record.environment}\`, project ${record.projectId}.`,
73
- '# Written by `fabrika platform install`. Every value below is a credential: keep this file private.',
74
- '# Zerops holds the same values as sensitive variables and does not hand them back.',
75
- '# Load it in a subshell: ( set -a && . <this file> && set +a && fabrika … )',
76
- ...lines,
77
- '',
78
- ].join('\n')
79
- }
80
-
81
- /** Create the file with mode 0600, failing rather than replacing one that appeared since the check. */
82
- export const writeSecretsFile = async (path: string, text: string): Promise<void> => {
83
- const file = await open(path, 'wx', 0o600)
84
- try {
85
- await file.writeFile(text)
86
- } finally {
87
- await file.close()
88
- }
27
+ return renderSecretsFile(
28
+ [
29
+ `fabrika on Zerops — installation \`${record.environment}\`, project ${record.projectId}.`,
30
+ 'Written by `fabrika platform install`. Every value below is a credential: keep this file private.',
31
+ 'Zerops holds the same values as sensitive variables and does not hand them back.',
32
+ ],
33
+ [
34
+ ['FABRIKA_ZEROPS_PROJECT_ID', record.projectId],
35
+ ['FABRIKA_ZEROPS_CLIENT_ID', record.clientId],
36
+ ['FABRIKA_IAM_PROVISIONING_KEY', secrets.provisioningKey],
37
+ ['FABRIKA_ZEROPS_ACCESS_TOKEN', record.zeropsAccessToken],
38
+ ['FABRIKA_IAM_RPC_KEY', secrets.rpcKey],
39
+ ['FABRIKA_IAM_PROXY_KEY', secrets.proxyKey],
40
+ ['OPERATIONS_SYNC_KEY', secrets.operationsSyncKey],
41
+ ['FABRIKA_CONTROL_VAULT_KEY', secrets.vaultKey],
42
+ ['FABRIKA_IAM_SIGNING_KEYS', secrets.signingKeys],
43
+ ],
44
+ )
89
45
  }
@@ -5,10 +5,15 @@
5
5
  import {
6
6
  action as consoleAction,
7
7
  confirmations,
8
+ generateInstallationSecrets,
8
9
  info as consoleInfo,
10
+ type InstallationSecrets,
9
11
  ok as consoleOk,
12
+ type PlatformHosts,
13
+ platformOrigin,
10
14
  step as consoleStep,
11
15
  warn as consoleWarn,
16
+ writeSecretsFile,
12
17
  } from '@fabrika/installation-init'
13
18
  import type { SchemaReconciler } from '@fabrika/provider-contract'
14
19
  import { createZeropsApi, defaultSleep, readableContent, type ZeropsNamespaceProxySource } from '../index'
@@ -24,7 +29,7 @@ import {
24
29
  type ResolvedService,
25
30
  } from './deploy'
26
31
  import { PLATFORM_PROXY_MANIFEST_TEMPLATE } from './generated/platform-proxy-manifest'
27
- import { derivePlatformHosts, type PlatformHosts, platformOrigin, ZEROPS_SUBDOMAIN_VARIABLE } from './hosts'
32
+ import { derivePlatformHosts, ZEROPS_SUBDOMAIN_VARIABLE } from './hosts'
28
33
  import { checkedEnvironmentName } from './init-options'
29
34
  import { assertRunning, type InstallCollaborators, type InstallPrompts } from './install-collaborators'
30
35
  import { GENERATED_SECRET_KEYS, installationServiceEnv, PASS_ONE_PROXY_MANIFEST, passOneProxyEnv, writeServiceEnv } from './install-environment'
@@ -39,11 +44,10 @@ import {
39
44
  resolvePlatformServices,
40
45
  serviceOf,
41
46
  } from './install-provisioning'
42
- import { renderSecretsFile, writeSecretsFile } from './install-secrets'
47
+ import { renderInstallationSecretsFile } from './install-secrets'
43
48
  import { explainMintFailure } from './install-token'
44
49
  import { prepareInstallationSource, prepareNamespaceProxySource, validateInstallationSetups } from './installation-source'
45
50
  import type { PreparedPlatformSource } from './platform-source'
46
- import { generateInstallationSecrets, type InstallationSecrets } from './secrets'
47
51
 
48
52
  export type { InstallCollaborators, InstallPrompts } from './install-collaborators'
49
53
  export { GENERATED_SECRET_KEYS, type InstallationEnvInput, installationServiceEnv } from './install-environment'
@@ -158,7 +162,7 @@ const installPreparedPlatform = async (
158
162
  log.step('Store the credentials locally')
159
163
  // BEFORE the first one is placed: Zerops keeps them sensitive and will not hand them back.
160
164
  await collaborators.storeSecrets(
161
- renderSecretsFile({ environment, projectId: project.id, clientId: input.clientId, secrets, zeropsAccessToken: token.token }),
165
+ renderInstallationSecretsFile({ environment, projectId: project.id, clientId: input.clientId, secrets, zeropsAccessToken: token.token }),
162
166
  )
163
167
  log.ok(`wrote ${input.secretsFile} (mode 0600) — every credential this installation was created with; nothing else holds a readable copy`)
164
168
 
@@ -12,9 +12,10 @@
12
12
  //
13
13
  // `fabrika.ref` names a published tag of `contember/fabrika-platform`, never a branch: a namespace
14
14
  // proxy and an installation both build from tags, so a moving ref would make "which version is this
15
- // installation running" unanswerable. `assertPinnedTag` refuses a branch when init writes the file, and
16
- // the generated workflow refuses one again at run time — two gates, because the file is operator-owned
17
- // after creation and the second gate is the only one that sees what the operator later wrote.
15
+ // installation running" unanswerable. `assertPinnedTag` (`@fabrika/installation-init`) refuses a branch
16
+ // when init writes the file, and the generated workflow refuses one again at run time — two gates,
17
+ // because the file is operator-owned after creation and the second gate is the only one that sees what
18
+ // the operator later wrote.
18
19
  //
19
20
  // ── The templates are not Cloudflare's ────────────────────────────────────────────────────────────
20
21
  //
@@ -23,6 +24,7 @@
23
24
  // pipeline; this one runs a single step because on Zerops the order is inside the command (ADR-0027).
24
25
  // Cloudflare builds a runner image and needs a GitHub App; Zerops has no runner at all (ADR-0003).
25
26
 
27
+ import { assertPinnedTag } from '@fabrika/installation-init'
26
28
  import { resolve } from 'node:path'
27
29
  import { z } from 'zod'
28
30
 
@@ -47,29 +49,6 @@ export interface SidecarValues {
47
49
  readonly fabrikaRef: string
48
50
  }
49
51
 
50
- /**
51
- * A published release tag: `v` and a semantic version.
52
- *
53
- * The same shape `scripts/release.ts` accepts, restated rather than imported — that script is not a
54
- * package and this check has to hold in a published tarball.
55
- */
56
- const TAG_PATTERN = /^v(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/
57
-
58
- /** The pinned ref, or an error naming what was found. A branch is refused, deliberately and loudly. */
59
- export const assertPinnedTag = (ref: string): string => {
60
- const trimmed = ref.trim()
61
- if (trimmed === '') {
62
- throw new Error('a published fabrika tag is required, for example v0.1.0')
63
- }
64
- if (!TAG_PATTERN.test(trimmed)) {
65
- throw new Error(
66
- `\`${trimmed}\` is not a published tag. A sidecar repository pins a TAG such as v0.1.0 and never a branch: `
67
- + 'an installation that tracked a moving ref could not say which version it runs (ADR-0025)',
68
- )
69
- }
70
- return trimmed
71
- }
72
-
73
52
  /** Substitute every placeholder in a template. */
74
53
  const render = async (name: string, values: SidecarValues): Promise<string> => {
75
54
  const raw = await Bun.file(resolve(TEMPLATES, name)).text()
@@ -92,6 +92,7 @@
92
92
  // arrives fully resolved). Only the proxy needs this, and it declares it explicitly below.
93
93
 
94
94
  import type { ZeropsHealthCheckSpec, ZeropsReadinessCheckSpec, ZeropsYamlSetupSpec, ZeropsYamlSpec } from '../../index'
95
+ import { FABRIKA_PROXY_MANIFEST_BRAND } from '../../proxy-manifest-carrier'
95
96
 
96
97
  // ── Readiness and liveness: two mechanisms, two questions ──────────────────────────────────────────
97
98
  //
@@ -401,6 +402,10 @@ const proxy: ZeropsYamlSetupSpec = {
401
402
  // it is the archive's data file (see the header).
402
403
  envVariables: {
403
404
  FABRIKA_PROXY_SESSION_OWNER_JSON: '${RUNTIME_FABRIKA_PROXY_SESSION_OWNER_JSON}',
405
+ // A marker, read by nothing at build time: a namespace pinned to this source has a manifest parser that knows
406
+ // `brand`, so Control may send the installation's client logo (ADR-0066). An older pin's parser refuses the
407
+ // key and fails its build, so it is sent only where this is (`zeropsProxyAcceptsBrand`).
408
+ [FABRIKA_PROXY_MANIFEST_BRAND]: '1',
404
409
  },
405
410
  // CGO off makes the binary static, which is what lets it run on Alpine/musl.
406
411
  prepareCommands: ['CGO_ENABLED=0 GOBIN=/tmp/gobin go install github.com/caddyserver/caddy/v2/cmd/caddy@v2.10.2'],
@@ -8,8 +8,8 @@
8
8
  // The pin is checked BEFORE anything leaves this machine: `assertPinnedTag` refuses a branch or a SHA
9
9
  // (ADR-0025) without a network call, so `--to=main` fails on the spot rather than after a clone.
10
10
 
11
- import { describeArgument, readFlag } from '@fabrika/installation-init'
12
- import { assertPinnedTag, defaultCheckoutDir } from './sidecar'
11
+ import { assertPinnedTag, describeArgument, readFlag } from '@fabrika/installation-init'
12
+ import { defaultCheckoutDir } from './sidecar'
13
13
 
14
14
  /** Where the sidecar repository this command edits is found. */
15
15
  export type UpgradeSidecar =