@fabrika/provider-zerops 0.8.1 → 0.9.0-cloudflare.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/package.json +16 -14
  2. package/src/api/client.ts +18 -0
  3. package/src/api/readers.ts +29 -0
  4. package/src/api/types.ts +64 -0
  5. package/src/api.ts +2 -0
  6. package/src/cli.ts +11 -0
  7. package/src/collaborators.ts +0 -14
  8. package/src/control.ts +35 -57
  9. package/src/execution.ts +113 -102
  10. package/src/index.ts +2 -0
  11. package/src/installation/admin.ts +1 -1
  12. package/src/installation/deploy-options.ts +1 -2
  13. package/src/installation/deploy.ts +225 -80
  14. package/src/installation/generated/platform-console-schema.ts +3 -0
  15. package/src/installation/generated/platform-proxy-manifest.ts +21 -1
  16. package/src/installation/guide.md +48 -18
  17. package/src/installation/hosts.ts +1 -7
  18. package/src/installation/index.ts +13 -16
  19. package/src/installation/init-options.ts +2 -1
  20. package/src/installation/install-environment.ts +47 -108
  21. package/src/installation/install-options.ts +3 -1
  22. package/src/installation/install-secrets.ts +24 -68
  23. package/src/installation/install.ts +9 -4
  24. package/src/installation/sidecar.ts +5 -26
  25. package/src/installation/topology/setups.ts +15 -4
  26. package/src/installation/topology/topology.ts +31 -21
  27. package/src/installation/upgrade-options.ts +2 -2
  28. package/src/managed-env.ts +1 -174
  29. package/src/manifest.ts +11 -14
  30. package/src/namespace/claims.ts +56 -1
  31. package/src/namespace/errors.ts +1 -1
  32. package/src/namespace/manifest.ts +6 -19
  33. package/src/namespace/provisioning.ts +24 -4
  34. package/src/namespace/proxy-attempt.ts +43 -2
  35. package/src/namespace/proxy-reads.ts +12 -22
  36. package/src/namespace/types.ts +2 -0
  37. package/src/namespace-cli.ts +152 -0
  38. package/src/plan.ts +2 -7
  39. package/src/provider.ts +128 -126
  40. package/src/proxy-manifest-carrier.ts +18 -0
  41. package/src/types.ts +0 -2
  42. package/types/src/api/client.d.ts +5 -0
  43. package/types/src/api/errors.d.ts +24 -0
  44. package/types/src/api/polling.d.ts +41 -0
  45. package/types/src/api/readers.d.ts +1115 -0
  46. package/types/src/api/service-env-limit.d.ts +17 -0
  47. package/types/src/api/service-env-response.d.ts +23 -0
  48. package/types/src/api/types.d.ts +687 -0
  49. package/types/src/api.d.ts +6 -0
  50. package/types/src/authoring.d.ts +9 -0
  51. package/types/src/cli-args.d.ts +19 -0
  52. package/types/src/cli.d.ts +13 -0
  53. package/types/src/collaborators.d.ts +16 -0
  54. package/types/src/compile.d.ts +91 -0
  55. package/types/src/control.d.ts +40 -0
  56. package/types/src/database-budget.d.ts +38 -0
  57. package/types/src/descriptor.d.ts +28 -0
  58. package/types/src/execution.d.ts +125 -0
  59. package/types/src/exposure.d.ts +47 -0
  60. package/types/src/index.d.ts +31 -0
  61. package/types/src/installation/admin-options.d.ts +24 -0
  62. package/types/src/installation/admin.d.ts +35 -0
  63. package/types/src/installation/checker-release.d.ts +12 -0
  64. package/types/src/installation/deploy-options.d.ts +37 -0
  65. package/types/src/installation/deploy.d.ts +136 -0
  66. package/types/src/installation/generated/platform-console-schema.d.ts +2 -0
  67. package/types/src/installation/generated/platform-proxy-manifest.d.ts +2 -0
  68. package/types/src/installation/hosts.d.ts +34 -0
  69. package/types/src/installation/index.d.ts +7 -0
  70. package/types/src/installation/init-options.d.ts +38 -0
  71. package/types/src/installation/init.d.ts +60 -0
  72. package/types/src/installation/install-collaborators.d.ts +24 -0
  73. package/types/src/installation/install-environment.d.ts +63 -0
  74. package/types/src/installation/install-options.d.ts +49 -0
  75. package/types/src/installation/install-project.d.ts +13 -0
  76. package/types/src/installation/install-provisioning.d.ts +58 -0
  77. package/types/src/installation/install-secrets.d.ts +17 -0
  78. package/types/src/installation/install-token.d.ts +10 -0
  79. package/types/src/installation/install.d.ts +23 -0
  80. package/types/src/installation/installation-source.d.ts +8 -0
  81. package/types/src/installation/log.d.ts +44 -0
  82. package/types/src/installation/machine-key.d.ts +19 -0
  83. package/types/src/installation/placement-options.d.ts +11 -0
  84. package/types/src/installation/platform-archive.d.ts +7 -0
  85. package/types/src/installation/platform-source.d.ts +8 -0
  86. package/types/src/installation/platform-upload.d.ts +3 -0
  87. package/types/src/installation/sidecar.d.ts +49 -0
  88. package/types/src/installation/source-options.d.ts +1 -0
  89. package/types/src/installation/topology/artifacts.d.ts +8 -0
  90. package/types/src/installation/topology/index.d.ts +5 -0
  91. package/types/src/installation/topology/installation-schemas.d.ts +2242 -0
  92. package/types/src/installation/topology/invariants.d.ts +23 -0
  93. package/types/src/installation/topology/setups.d.ts +59 -0
  94. package/types/src/installation/topology/topology.d.ts +153 -0
  95. package/types/src/installation/topology/validate.d.ts +11 -0
  96. package/types/src/installation/upgrade-options.d.ts +26 -0
  97. package/types/src/installation/upgrade-output.d.ts +6 -0
  98. package/types/src/installation/upgrade.d.ts +88 -0
  99. package/types/src/managed-env.d.ts +24 -0
  100. package/types/src/manifest.d.ts +204 -0
  101. package/types/src/namespace/claims.d.ts +16 -0
  102. package/types/src/namespace/codec.d.ts +334 -0
  103. package/types/src/namespace/constants.d.ts +25 -0
  104. package/types/src/namespace/errors.d.ts +15 -0
  105. package/types/src/namespace/manifest.d.ts +4 -0
  106. package/types/src/namespace/operator.d.ts +12 -0
  107. package/types/src/namespace/provisioning.d.ts +4 -0
  108. package/types/src/namespace/proxy-attempt.d.ts +39 -0
  109. package/types/src/namespace/proxy-reads.d.ts +22 -0
  110. package/types/src/namespace/proxy-secrets.d.ts +8 -0
  111. package/types/src/namespace/proxy-source.d.ts +74 -0
  112. package/types/src/namespace/topology.d.ts +9 -0
  113. package/types/src/namespace/types.d.ts +101 -0
  114. package/types/src/namespace/update.d.ts +6 -0
  115. package/types/src/namespace/validation.d.ts +12 -0
  116. package/types/src/namespace-cli.d.ts +2 -0
  117. package/types/src/namespace.d.ts +9 -0
  118. package/types/src/plan.d.ts +11 -0
  119. package/types/src/provider.d.ts +63 -0
  120. package/types/src/proxy-manifest-carrier.d.ts +24 -0
  121. package/types/src/schema.generated.d.ts +376 -0
  122. package/types/src/service-names.d.ts +4 -0
  123. package/types/src/service-references.d.ts +13 -0
  124. package/types/src/source/constants.d.ts +6 -0
  125. package/types/src/source/port.d.ts +60 -0
  126. package/types/src/source/types.d.ts +4 -0
  127. package/types/src/source/validation.d.ts +19 -0
  128. package/types/src/source.d.ts +2 -0
  129. package/types/src/types.d.ts +119 -0
  130. package/types/src/version-attempt.d.ts +8 -0
  131. package/src/installation/mcp.ts +0 -102
  132. package/src/installation/proxy-manifest.ts +0 -214
  133. package/src/installation/secrets.ts +0 -78
@@ -31,22 +31,18 @@ import { parsePlatformUpgradeArgs, UPGRADE_DRY_RUN_FLAG } from './upgrade-option
31
31
  export { PLATFORM_CONCURRENT_DEPLOY, PLATFORM_DEPLOY_ORDER, PLATFORM_PROXY_SERVICE, PLATFORM_SEQUENTIAL_DEPLOY } from './deploy'
32
32
  export { PLATFORM_CONSOLE_APP_SCHEMA } from './generated/platform-console-schema'
33
33
  export { PLATFORM_PROXY_MANIFEST_TEMPLATE } from './generated/platform-proxy-manifest'
34
- export { type IamOAuthConfig, type McpClient, parseMcpClientsJson, platformIamOAuthConfig } from './mcp'
35
- export {
36
- type PlatformOAuthResourceTemplate,
37
- type PlatformProxyApp,
38
- platformProxyAppFor,
39
- type PlatformProxyComposition,
40
- type PlatformProxyManifest,
41
- type PlatformProxyManifestTemplate,
42
- type PlatformProxyPlacement,
43
- type PlatformProxyService,
44
- platformServiceTableDigest,
45
- resolvePlatformProxyManifest,
46
- } from './proxy-manifest'
47
34
 
48
35
  const EMAIL_PROVIDERS = Object.keys(builtinEmailProviders).join(', ')
49
- const EMAIL_PROVIDER_VARIABLES = Object.values(builtinEmailProviders).flatMap((provider) => provider.variables.map((variable) => variable.name))
36
+ /** One line per provider: its variables, each marked secret or optional where it is. */
37
+ const EMAIL_PROVIDER_VARIABLES = Object.entries(builtinEmailProviders)
38
+ .map(([name, provider]) => {
39
+ const variables = provider.variables.map((variable) => {
40
+ const marks = [variable.secret ? 'secret' : '', variable.required ? '' : 'optional'].filter((mark) => mark !== '')
41
+ return marks.length === 0 ? variable.name : `${variable.name} (${marks.join(', ')})`
42
+ })
43
+ return `\t ${name}: ${variables.join(', ')}`
44
+ })
45
+ .join('\n')
50
46
 
51
47
  const USAGE = `Zerops installation
52
48
 
@@ -132,7 +128,7 @@ Options (a flag beats the environment variable beside it):
132
128
  FABRIKA_ZEROPS_ACCESS_TOKEN required Zerops access token, environment only and no flag
133
129
  FABRIKA_ZEROPS_API_URL optional region API base, when not the default
134
130
  FABRIKA_EMAIL_<PROVIDER>_* the selected provider's variables, environment only and no flag:
135
- ${EMAIL_PROVIDER_VARIABLES.join(', ')}
131
+ ${EMAIL_PROVIDER_VARIABLES}
136
132
 
137
133
  With a mail provider, IAM sends its own mail and Control hands the same transport to every application
138
134
  whose job declares \`mailer: 'installation'\`. To set or rotate it later, see \`platform guide\`.
@@ -254,7 +250,8 @@ process listing:
254
250
  (a --dry-run does not need it)
255
251
  FABRIKA_ZEROPS_API_URL optional region API base, when not the default
256
252
 
257
- This command writes NO credential. Every secret an installation holds is placed at bring-up.
253
+ This command writes one credential, derived from FABRIKA_IAM_PROVISIONING_KEY: the secret the proxy
254
+ proves its requests to IAM with. Every other secret an installation holds is placed at bring-up.
258
255
 
259
256
  Install and deploy require --source-dir explicitly. Run the CLI from that same clean full checkout;
260
257
  a packaged CLI cannot supply another revision's manifest or schema. --from-git and
@@ -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,23 +22,16 @@ 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}`,
30
29
  endpoint: `\${${service}_apiUrl}`,
31
30
  })
32
31
 
33
- /** IAM and control share `db` and `storage` on both tiers; `standard` gives Operations its own pair. */
32
+ /** IAM and control share `db` and `storage` on both tiers; Operations has its own database on both, and its own bucket on `standard`. */
34
33
  const OPERATIONS_DATA: Readonly<Record<PlatformTier, { readonly database: string; readonly storage: string }>> = {
35
- light: { database: 'db', storage: 'storage' },
34
+ light: { database: 'operationsdb', storage: 'storage' },
36
35
  standard: { database: 'operationsdb', storage: 'operationsstorage' },
37
36
  }
38
37
 
@@ -56,6 +55,8 @@ export const PASS_ONE_PROXY_MANIFEST = encodeProxyManifestJson({ apps: [] })
56
55
  /** What `installationServiceEnv` needs. Everything in it is settled before the first write. */
57
56
  export interface InstallationEnvInput {
58
57
  readonly environment: string
58
+ /** The installation's own Zerops project, which no deployment namespace may adopt. */
59
+ readonly projectId: string
59
60
  readonly tier: PlatformTier
60
61
  readonly scheme: 'http' | 'https'
61
62
  readonly hosts: PlatformHosts
@@ -70,132 +71,70 @@ export interface InstallationEnvInput {
70
71
  }
71
72
 
72
73
  /**
73
- * 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.
74
76
  *
75
77
  * Pure, and exported, because the matrix is the thing most worth pinning: a name that is right on
76
- * Cloudflare and wrong on Bun is a silent no-op rather than a failure. Two spelling rules run through
77
- * it, both verified against the runtimes rather than assumed:
78
- *
79
- * • IAM's Bun runtime reads `ISSUER` and `ENVIRONMENT` UNPREFIXED and everything else with the
80
- * `FABRIKA_IAM_` prefix (`packages/iam/src/node/runtime.ts`). The prefixed spellings below are the
81
- * ones that file actually reads; the unprefixed ones are the ones it actually reads too.
82
- * • No key any service declares in its own `zerops.yaml` appears here — `PORT`,
83
- * `FABRIKA_IAM_RPC_URL`, `FABRIKA_OPERATIONS_URL`, `FABRIKA_CONTROL_ASSETS_DIR`,
84
- * `FABRIKA_CONTROL_URL`, `FABRIKA_PROXY_MANIFEST`, `FABRIKA_PROXY_PORT`. The env API refuses those
85
- * (`provider-zerops/src/api.ts`), because the conflict cannot be resolved to a record id.
86
- *
87
- * The proxy manifest is deliberately absent: pass 1 writes an empty one and `deployPlatform` owns the
88
- * 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.
89
82
  */
90
83
  export const installationServiceEnv = (input: InstallationEnvInput): ReadonlyMap<PlatformDeployService, ReadonlyMap<string, string>> => {
91
84
  const { secrets } = input
92
- const iamOrigin = platformOrigin(input.scheme, input.hosts.iam)
93
- const operationsOrigin = platformOrigin(input.scheme, input.hosts.operations)
94
- const platformDatabase = postgresUrl('db')
95
- const runLogs = storageOf('storage')
96
85
  const operationsData = OPERATIONS_DATA[input.tier]
97
- 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
+ })
98
101
  const control = new Map([
99
- ['FABRIKA_CONTROL_DATABASE_URL', platformDatabase],
100
- ['ENVIRONMENT', input.environment],
101
- // A bare HOST, not an origin: `controlPublicOrigin` accepts either.
102
- ['FABRIKA_CONTROL_DOMAIN', input.hosts.control],
103
- ['FABRIKA_IAM_ISSUER', iamOrigin],
104
- ['OPERATIONS_ARTIFACT_ORIGIN', operationsOrigin],
105
- ['FABRIKA_CONTROL_RUN_LOGS_BUCKET', runLogs.bucket],
106
- ['FABRIKA_CONTROL_RUN_LOGS_ACCESS_KEY_ID', runLogs.accessKeyId],
107
- ['FABRIKA_CONTROL_RUN_LOGS_SECRET_ACCESS_KEY', runLogs.secretAccessKey],
108
- ['FABRIKA_CONTROL_RUN_LOGS_ENDPOINT', runLogs.endpoint],
109
- ['FABRIKA_IAM_RPC_KEY', secrets.rpcKey],
110
- ['OPERATIONS_SYNC_KEY', secrets.operationsSyncKey],
111
- ['FABRIKA_CONTROL_VAULT_KEY', secrets.vaultKey],
112
- ['FABRIKA_IAM_PROVISIONING_KEY', secrets.provisioningKey],
102
+ ...fabrika.control,
113
103
  ['FABRIKA_ZEROPS_CLIENT_ID', input.clientId],
114
104
  [FABRIKA_ZEROPS_PROXY_SOURCE_JSON, serializeNamespaceProxySource(input.proxySource)],
115
105
  // The PUBLIC IAM origin an application project's proxy reaches: there is no private network
116
106
  // between Zerops projects (ADR-0006).
117
- ['FABRIKA_ZEROPS_PROXY_IAM_URL', iamOrigin],
107
+ ['FABRIKA_ZEROPS_PROXY_IAM_URL', platformOrigin(input.scheme, input.hosts.iam)],
118
108
  ['FABRIKA_ZEROPS_PROXY_IAM_KEY', secrets.proxyKey],
119
109
  ['FABRIKA_ZEROPS_ACCESS_TOKEN', input.zeropsAccessToken],
110
+ ['FABRIKA_ZEROPS_PLATFORM_PROJECT_ID', input.projectId],
120
111
  ])
121
112
  if (input.apiBaseUrl !== undefined) {
122
113
  // Only when the installation is not on the default region. Control spells it `_API_BASE_URL` while
123
114
  // the CLI reads `FABRIKA_ZEROPS_API_URL`; they are different processes reading different sources.
124
115
  control.set('FABRIKA_ZEROPS_API_BASE_URL', input.apiBaseUrl)
125
116
  }
126
- const email = input.email.kind === 'none' ? [] : [...input.email.variables]
127
- for (const [key, value] of email) control.set(key, value)
128
117
  return new Map<PlatformDeployService, ReadonlyMap<string, string>>([
129
- [
130
- 'iam',
131
- new Map([
132
- ['FABRIKA_IAM_DATABASE_URL', platformDatabase],
133
- ['ENVIRONMENT', input.environment],
134
- ['ISSUER', iamOrigin],
135
- // Password only, by decision: OIDC needs an identity provider the operator has not been
136
- // asked for. Both are STATED because IAM's own defaults are the other way round — OIDC on,
137
- // password off — and it refuses to boot with neither.
138
- ['FABRIKA_IAM_OIDC_ENABLED', 'false'],
139
- ['FABRIKA_IAM_PASSWORD_ENABLED', 'true'],
140
- ['FABRIKA_IAM_SIGNING_KEYS', secrets.signingKeys],
141
- ['FABRIKA_IAM_RPC_KEY', secrets.rpcKey],
142
- ['FABRIKA_IAM_PROXY_KEY', secrets.proxyKey],
143
- ['FABRIKA_IAM_PROVISIONING_KEY', secrets.provisioningKey],
144
- ...email,
145
- ]),
146
- ],
147
- [
148
- 'operations',
149
- new Map([
150
- ['FABRIKA_OPERATIONS_DATABASE_URL', postgresUrl(operationsData.database)],
151
- ['FABRIKA_OPERATIONS_BLOB_BUCKET', operationsBlobs.bucket],
152
- ['FABRIKA_OPERATIONS_BLOB_ACCESS_KEY_ID', operationsBlobs.accessKeyId],
153
- ['FABRIKA_OPERATIONS_BLOB_SECRET_ACCESS_KEY', operationsBlobs.secretAccessKey],
154
- ['FABRIKA_OPERATIONS_BLOB_ENDPOINT', operationsBlobs.endpoint],
155
- ['FABRIKA_OPERATIONS_PUBLIC_HOST', input.hosts.operations],
156
- ['ENVIRONMENT', input.environment],
157
- ['FABRIKA_IAM_ISSUER', iamOrigin],
158
- ['OPERATIONS_SYNC_KEY', secrets.operationsSyncKey],
159
- ['FABRIKA_IAM_RPC_KEY', secrets.rpcKey],
160
- ]),
161
- ],
118
+ ['iam', fabrika.iam],
119
+ ['operations', fabrika.operations],
162
120
  ['control', control],
163
- [
164
- 'proxy',
165
- new Map([
166
- ['ENVIRONMENT', input.environment],
167
- // Compared byte-for-byte against a token's `iss`, so this is the PUBLIC issuer and not the
168
- // private `iam:3000` pass 1 wrote.
169
- ['FABRIKA_IAM_ISSUER', iamOrigin],
170
- ['FABRIKA_IAM_KEY', secrets.proxyKey],
171
- ['FABRIKA_PROXY_SESSION_OWNER_JSON', JSON.stringify({ issuer: iamOrigin, upstream: PRIVATE_IAM_UPSTREAM })],
172
- ]),
173
- ],
121
+ ['proxy', fabrika.proxy],
174
122
  ])
175
123
  }
176
124
 
177
125
  /**
178
- * 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.
179
128
  *
180
129
  * Their presence on a live service is what "this project already holds an installation" means, and it is
181
130
  * checked by KEY only — a value is never read back for comparison. `__tests__/install.test.ts` pins this
182
131
  * against the matrix above, so a new generated secret cannot be added without appearing here.
183
132
  */
184
133
  export const GENERATED_SECRET_KEYS: ReadonlyMap<PlatformDeployService, readonly string[]> = new Map<PlatformDeployService, readonly string[]>([
185
- ['iam', ['FABRIKA_IAM_SIGNING_KEYS', 'FABRIKA_IAM_RPC_KEY', 'FABRIKA_IAM_PROXY_KEY', 'FABRIKA_IAM_PROVISIONING_KEY']],
186
- ['operations', ['OPERATIONS_SYNC_KEY', 'FABRIKA_IAM_RPC_KEY']],
187
- [
188
- 'control',
189
- [
190
- 'FABRIKA_IAM_RPC_KEY',
191
- 'OPERATIONS_SYNC_KEY',
192
- 'FABRIKA_CONTROL_VAULT_KEY',
193
- 'FABRIKA_IAM_PROVISIONING_KEY',
194
- 'FABRIKA_ZEROPS_PROXY_IAM_KEY',
195
- 'FABRIKA_ZEROPS_ACCESS_TOKEN',
196
- ],
197
- ],
198
- ['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],
199
138
  ])
200
139
 
201
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
 
@@ -170,6 +174,7 @@ const installPreparedPlatform = async (
170
174
  log.step('Write every remaining variable')
171
175
  const desired = installationServiceEnv({
172
176
  environment,
177
+ projectId: project.id,
173
178
  tier: input.tier,
174
179
  scheme: input.scheme,
175
180
  hosts,
@@ -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()
@@ -47,7 +47,9 @@
47
47
  // into the platform's variable store rather than carrying a credential. That reasoning is still sound
48
48
  // and it is no longer sufficient, because a reference names a SERVICE — and which data services exist
49
49
  // is exactly what differs between the `standard` and `light` tiers (`./topology.ts`). One committed
50
- // file cannot say `${operationsdb_connectionString}` on behalf of a tier that has no `operationsdb`.
50
+ // file cannot say `${operationsstorage_bucketName}` on behalf of a tier that has no `operationsstorage`,
51
+ // nor `${operationsdb_connectionString}` on behalf of a light installation created before Operations
52
+ // had a database of its own.
51
53
  // So the file's own rule decides it: a data-service reference is not true for every installation, and
52
54
  // it moves to the env API alongside the origins and the secrets.
53
55
  //
@@ -90,6 +92,7 @@
90
92
  // arrives fully resolved). Only the proxy needs this, and it declares it explicitly below.
91
93
 
92
94
  import type { ZeropsHealthCheckSpec, ZeropsReadinessCheckSpec, ZeropsYamlSetupSpec, ZeropsYamlSpec } from '../../index'
95
+ import { FABRIKA_PROXY_MANIFEST_BRAND } from '../../proxy-manifest-carrier'
93
96
 
94
97
  // ── Readiness and liveness: two mechanisms, two questions ──────────────────────────────────────────
95
98
  //
@@ -181,6 +184,8 @@ const WORKSPACE_DEPLOY_FILES = ['package.json', 'bun.lock', 'node_modules', 'pac
181
184
  * since the proxy is the only publicly routed component and must not also be able to write audit rows),
182
185
  * `FABRIKA_IAM_PROVISIONING_KEY`. Both shared secrets must be at least 32 characters; a shorter one fails
183
186
  * the boot rather than standing in front of an internet-reachable surface while looking configured.
187
+ * `FABRIKA_INSTALLATION_PROXY_SECRET_IAM`, which `platform deploy` derives and writes, lets IAM tell the proxy's
188
+ * client-address and request-id headers from a private caller's.
184
189
  */
185
190
  const iam: ZeropsYamlSetupSpec = {
186
191
  setup: 'iam',
@@ -291,8 +296,9 @@ const control: ZeropsYamlSetupSpec = {
291
296
  * `FABRIKA_OPERATIONS_BLOB_*` coordinates, `ENVIRONMENT`, `FABRIKA_OPERATIONS_PUBLIC_HOST` and
292
297
  * `FABRIKA_IAM_ISSUER` (the public IAM issuer). `FABRIKA_OPERATIONS_DATABASE_URL` takes the canonical
293
298
  * form above. On the `standard` tier the data references are `${operationsdb_*}` and
294
- * `${operationsstorage_*}`; on `light` they are `${db_*}` and `${storage_*}`, which is precisely why
295
- * they cannot be written here.
299
+ * `${operationsstorage_*}`; on `light` they are `${operationsdb_*}` and `${storage_*}` (`${db_*}` on a
300
+ * light installation created before Operations had its own database), which is precisely why they cannot
301
+ * be written here.
296
302
  *
297
303
  * Secrets (`envSecrets`): `OPERATIONS_SYNC_KEY`, shared only with control for catalog projection, and
298
304
  * `FABRIKA_IAM_RPC_KEY`, used only for Operations → IAM management RPC.
@@ -331,7 +337,8 @@ const operations: ZeropsYamlSetupSpec = {
331
337
  * IAM service's PUBLIC origin in an apps project, because there is no private network between projects
332
338
  * (ADR-0006). The manifest is not a variable; see below.
333
339
  *
334
- * Secret (`envSecrets`): `FABRIKA_IAM_KEY` — the value IAM knows as `FABRIKA_IAM_PROXY_KEY`.
340
+ * Secret (`envSecrets`): `FABRIKA_IAM_KEY` — the value IAM knows as `FABRIKA_IAM_PROXY_KEY`. In the platform project
341
+ * also `FABRIKA_INSTALLATION_PROXY_SECRET_IAM`, which Caddy sends on IAM's route and IAM holds too.
335
342
  *
336
343
  * ── How the manifest reaches the build ───────────────────────────────────────────────────────────
337
344
  *
@@ -395,6 +402,10 @@ const proxy: ZeropsYamlSetupSpec = {
395
402
  // it is the archive's data file (see the header).
396
403
  envVariables: {
397
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',
398
409
  },
399
410
  // CGO off makes the binary static, which is what lets it run on Alpine/musl.
400
411
  prepareCommands: ['CGO_ENABLED=0 GOBIN=/tmp/gobin go install github.com/caddyserver/caddy/v2/cmd/caddy@v2.10.2'],