@fabrika/provider-zerops 0.0.26 → 0.3.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 (83) hide show
  1. package/package.json +18 -5
  2. package/src/api/client.ts +522 -0
  3. package/src/api/errors.ts +50 -0
  4. package/src/api/polling.ts +59 -0
  5. package/src/api/readers.ts +185 -0
  6. package/src/api/service-env-response.ts +34 -0
  7. package/src/api/types.ts +602 -0
  8. package/src/api.ts +29 -1378
  9. package/src/authoring.ts +7 -10
  10. package/src/cli-args.ts +0 -5
  11. package/src/cli.ts +3 -28
  12. package/src/collaborators.ts +2 -4
  13. package/src/compile.ts +75 -20
  14. package/src/control.ts +217 -447
  15. package/src/descriptor.ts +135 -0
  16. package/src/index.ts +24 -128
  17. package/src/installation/admin-options.ts +128 -0
  18. package/src/installation/admin.ts +123 -0
  19. package/src/installation/deploy-options.ts +155 -0
  20. package/src/installation/deploy.ts +668 -0
  21. package/src/installation/generated/platform-console-schema.ts +41 -0
  22. package/src/installation/generated/platform-proxy-manifest.ts +72 -0
  23. package/src/installation/hosts.ts +117 -0
  24. package/src/installation/index.ts +366 -0
  25. package/src/installation/init.ts +521 -0
  26. package/src/installation/install-collaborators.ts +28 -0
  27. package/src/installation/install-environment.ts +193 -0
  28. package/src/installation/install-options.ts +193 -0
  29. package/src/installation/install-project.ts +201 -0
  30. package/src/installation/install-provisioning.ts +185 -0
  31. package/src/installation/install.ts +286 -0
  32. package/src/installation/installation-source.ts +42 -0
  33. package/src/installation/log.ts +77 -0
  34. package/src/installation/machine-key.ts +65 -0
  35. package/src/installation/manifest.ts +95 -0
  36. package/src/installation/placement-options.ts +33 -0
  37. package/src/installation/platform-source.ts +106 -0
  38. package/src/installation/platform-upload.ts +34 -0
  39. package/src/installation/proxy-manifest.ts +113 -0
  40. package/src/installation/secrets.ts +79 -0
  41. package/src/installation/sidecar.ts +122 -0
  42. package/src/installation/source-options.ts +12 -0
  43. package/src/installation/templates/README.md +116 -0
  44. package/src/installation/templates/fabrika.ref +1 -0
  45. package/src/installation/templates/gitignore +3 -0
  46. package/src/installation/templates/platform.yml +144 -0
  47. package/src/installation/topology/artifacts.ts +141 -0
  48. package/src/installation/topology/index.ts +13 -0
  49. package/src/installation/topology/installation-schemas.ts +225 -0
  50. package/src/installation/topology/invariants.ts +86 -0
  51. package/src/installation/topology/setups.ts +442 -0
  52. package/src/installation/topology/topology.ts +501 -0
  53. package/src/installation/topology/validate.ts +26 -0
  54. package/src/installation/upgrade-options.ts +108 -0
  55. package/src/installation/upgrade-output.ts +31 -0
  56. package/src/installation/upgrade.ts +373 -0
  57. package/src/managed-env.ts +105 -0
  58. package/src/manifest.ts +208 -315
  59. package/src/namespace/claims.ts +77 -0
  60. package/src/namespace/codec.ts +179 -0
  61. package/src/namespace/constants.ts +30 -0
  62. package/src/namespace/errors.ts +60 -0
  63. package/src/namespace/manifest.ts +28 -0
  64. package/src/namespace/operator.ts +151 -0
  65. package/src/namespace/provisioning.ts +482 -0
  66. package/src/namespace/proxy-attempt.ts +220 -0
  67. package/src/namespace/proxy-source.ts +22 -0
  68. package/src/namespace/topology.ts +115 -0
  69. package/src/namespace/types.ts +114 -0
  70. package/src/namespace.ts +34 -1260
  71. package/src/plan.ts +21 -23
  72. package/src/provider.ts +399 -254
  73. package/src/service-references.ts +28 -0
  74. package/src/source/constants.ts +11 -0
  75. package/src/source/port.ts +49 -0
  76. package/src/source/types.ts +4 -0
  77. package/src/source/validation.ts +72 -0
  78. package/src/source.ts +2 -1220
  79. package/src/types.ts +8 -7
  80. package/src/version-attempt.ts +18 -0
  81. package/src/codec.ts +0 -108
  82. package/src/namespace-command.ts +0 -209
  83. package/src/source-connection.ts +0 -344
@@ -0,0 +1,220 @@
1
+ import { followLeaseLoss, isLeaseLoss, ProviderNamespaceError } from '@fabrika/provider-contract'
2
+ import { encodeProxyManifestJson, FABRIKA_PROXY_MANIFEST_JSON, type ProxyManifest } from '@fabrika/proxy-core/manifest'
3
+ import { ZEROPS_ACTIVE, ZEROPS_TERMINAL, type ZeropsApi, ZeropsApiError, type ZeropsAppVersion } from '../api'
4
+ import type { Sleeper } from '../collaborators'
5
+ import { prepareZeropsDescriptor } from '../descriptor'
6
+ import { isZeropsUploadDestination } from '../source'
7
+ import type { ZeropsSourcePort } from '../source/port'
8
+ import { deleteVersion } from '../version-attempt'
9
+ import { ZEROPS_NAMESPACE_IAM_ISSUER_VARIABLE, ZEROPS_NAMESPACE_IAM_KEY_VARIABLE } from './constants'
10
+ import { verifyZeropsNamespaceProxySource } from './proxy-source'
11
+ import type { ZeropsNamespaceProxyBuild, ZeropsNamespaceTarget } from './types'
12
+
13
+ export type ZeropsProxyApi = Pick<
14
+ ZeropsApi,
15
+ 'listServiceEnv' | 'putServiceEnv' | 'createAppVersion' | 'deleteAppVersion' | 'buildAndDeployAppVersion' | 'getAppVersion'
16
+ >
17
+
18
+ export interface ZeropsProxyRepublishInput {
19
+ readonly target: ZeropsNamespaceTarget
20
+ readonly manifest: ProxyManifest
21
+ readonly source?: ZeropsSourcePort
22
+ readonly api: ZeropsProxyApi
23
+ readonly iamUrl: string
24
+ readonly iamKey: string
25
+ /** Aborts with `lease_lost` when the namespace lease goes, and with `cancel_requested` when a deploy is cancelled. */
26
+ readonly signal: AbortSignal
27
+ readonly wait: 'started' | 'active'
28
+ /** Persist the target; the caller fences the write on the namespace lease. */
29
+ readonly checkpoint: (target: ZeropsNamespaceTarget) => Promise<void>
30
+ readonly boundary: () => Promise<void>
31
+ readonly sleep: Sleeper
32
+ }
33
+
34
+ const POLL_INTERVAL_MS = 3000
35
+ const POLL_DEADLINE_MS = 70 * 60 * 1000
36
+ const USER_DATA_SYNC_ATTEMPTS = 10
37
+ const SESSION_OWNER_VARIABLE = 'FABRIKA_PROXY_SESSION_OWNER_JSON'
38
+
39
+ const unresolved = (id: string): ProviderNamespaceError =>
40
+ new ProviderNamespaceError(`Proxy version ${id} remains unresolved; reconcile this namespace before another build`, 'proxyAttemptUnresolved', true)
41
+
42
+ const digest = async (text: string): Promise<string> =>
43
+ Array.from(new Uint8Array(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(text))), byte => byte.toString(16).padStart(2, '0')).join(
44
+ '',
45
+ )
46
+
47
+ const writeProxyIam = async (input: ZeropsProxyRepublishInput, serviceId: string): Promise<void> => {
48
+ const { api, signal } = input
49
+ const variables = await api.listServiceEnv({ serviceId, signal })
50
+ if (variables.find(variable => variable.key === SESSION_OWNER_VARIABLE)?.content !== 'null') {
51
+ await api.putServiceEnv({ serviceId, key: SESSION_OWNER_VARIABLE, value: 'null', signal })
52
+ }
53
+ if (variables.find(variable => variable.key === ZEROPS_NAMESPACE_IAM_ISSUER_VARIABLE)?.content !== input.iamUrl) {
54
+ await api.putServiceEnv({ serviceId, key: ZEROPS_NAMESPACE_IAM_ISSUER_VARIABLE, value: input.iamUrl, signal })
55
+ }
56
+ // Secret read-back is not trustworthy, so overwrite it without inspecting its value.
57
+ await api.putServiceEnv({ serviceId, key: ZEROPS_NAMESPACE_IAM_KEY_VARIABLE, value: input.iamKey, signal })
58
+ }
59
+
60
+ /**
61
+ * Rebuild the namespace proxy with `manifest` (ADR-0044). The attempt is recorded as `proxyBuild` and never
62
+ * resumed: an inherited attempt not yet `triggered` is deleted, a triggered one is polled to a terminal state.
63
+ * A lost lease deletes nothing; a cancelled deploy leaves a triggered attempt recorded for reconciliation.
64
+ */
65
+ export async function republishProxy(input: ZeropsProxyRepublishInput): Promise<ZeropsNamespaceTarget> {
66
+ const pin = input.target.proxySource
67
+ const serviceId = input.target.proxyServiceId
68
+ if (serviceId === undefined) throw new Error('Zerops namespace has no proxy service id')
69
+ await verifyZeropsNamespaceProxySource(pin)
70
+ const manifest = encodeProxyManifestJson(input.manifest)
71
+ // The attempt's identity covers the IAM configuration too, so a rotated issuer or key forces a rebuild.
72
+ const manifestSha256 = await digest(`${manifest}\n${input.iamUrl}\n${await digest(input.iamKey)}`)
73
+ let target = input.target
74
+
75
+ const save = async (next: ZeropsNamespaceTarget): Promise<void> => {
76
+ await input.checkpoint(next)
77
+ target = next
78
+ }
79
+
80
+ /** Observe one version until terminal; with `started`, a running build of this manifest is answer enough. */
81
+ const settle = async (build: ZeropsNamespaceProxyBuild): Promise<'settled' | 'running'> => {
82
+ const deadline = Date.now() + POLL_DEADLINE_MS
83
+ for (;;) {
84
+ await input.boundary()
85
+ input.signal.throwIfAborted()
86
+ let version: ZeropsAppVersion
87
+ try {
88
+ version = await input.api.getAppVersion({ appVersionId: build.appVersionId, signal: input.signal })
89
+ } catch {
90
+ input.signal.throwIfAborted()
91
+ throw unresolved(build.appVersionId)
92
+ }
93
+ if (
94
+ version.id !== build.appVersionId || (version.serviceStackId !== undefined && version.serviceStackId !== serviceId)
95
+ || (version.projectId !== undefined && target.projectId !== undefined && version.projectId !== target.projectId)
96
+ ) throw unresolved(build.appVersionId)
97
+ if (version.status !== undefined && ZEROPS_TERMINAL.has(version.status)) {
98
+ const active = version.status === ZEROPS_ACTIVE
99
+ await save({
100
+ ...target,
101
+ proxyBuild: undefined,
102
+ ...(active
103
+ ? {
104
+ proxyDeployment: {
105
+ appVersionId: version.id,
106
+ repository: pin.repository,
107
+ commitSha: pin.commitSha,
108
+ descriptorSha256: pin.descriptor.sha256,
109
+ ...(build.manifestSha256 === undefined ? {} : { manifestSha256: build.manifestSha256 }),
110
+ },
111
+ }
112
+ : { ready: false }),
113
+ })
114
+ if (!active) throw new ProviderNamespaceError(`Proxy version ${version.id} finished as ${version.status}`, 'proxyDeployFailed', false)
115
+ return 'settled'
116
+ }
117
+ if (input.wait === 'started') {
118
+ if (build.manifestSha256 === manifestSha256 && version.status !== 'UPLOADING') return 'running'
119
+ throw unresolved(build.appVersionId)
120
+ }
121
+ if (Date.now() >= deadline) throw unresolved(build.appVersionId)
122
+ await input.sleep(POLL_INTERVAL_MS, input.signal)
123
+ }
124
+ }
125
+
126
+ const inherited = target.proxyBuild
127
+ if (inherited !== undefined && !inherited.triggered) {
128
+ input.signal.throwIfAborted()
129
+ try {
130
+ await deleteVersion(input.api, inherited.appVersionId, input.signal)
131
+ } catch {
132
+ input.signal.throwIfAborted()
133
+ throw unresolved(inherited.appVersionId)
134
+ }
135
+ await save({ ...target, proxyBuild: undefined })
136
+ } else if (inherited !== undefined) {
137
+ if (await settle(inherited) === 'running') return target
138
+ if (inherited.manifestSha256 === manifestSha256) return target
139
+ }
140
+
141
+ if (input.source === undefined) throw new Error('Zerops namespace source client is required')
142
+ const runId = crypto.randomUUID()
143
+ const resolved = await input.source.resolve({
144
+ repository: pin.repository,
145
+ requestedRef: pin.commitSha,
146
+ expectedCommitSha: pin.commitSha,
147
+ descriptorSha256: pin.descriptor.sha256,
148
+ signal: input.signal,
149
+ })
150
+ if (resolved.commitSha !== pin.commitSha || resolved.descriptorSha256 !== pin.descriptor.sha256) {
151
+ throw new Error('Proxy source resolution identity mismatch')
152
+ }
153
+ await input.boundary()
154
+ await input.api.putServiceEnv({ serviceId, key: FABRIKA_PROXY_MANIFEST_JSON, value: manifest, signal: input.signal })
155
+ await writeProxyIam(input, serviceId)
156
+ input.signal.throwIfAborted()
157
+ const reserved = await input.api.createAppVersion({ serviceId, name: runId, signal: input.signal })
158
+ let build: ZeropsNamespaceProxyBuild = { appVersionId: reserved.id, triggered: false, manifestSha256 }
159
+ let accepted = false
160
+ try {
161
+ await save({ ...target, proxyBuild: build })
162
+ input.signal.throwIfAborted()
163
+ if (!isZeropsUploadDestination(reserved.uploadUrl)) throw new Error(`Proxy version ${reserved.id} has an unexpected upload destination`)
164
+ const uploaded = await input.source.upload({
165
+ repository: pin.repository,
166
+ commitSha: pin.commitSha,
167
+ descriptorSha256: pin.descriptor.sha256,
168
+ archive: { kind: 'platform' },
169
+ uploadUrl: reserved.uploadUrl,
170
+ signal: input.signal,
171
+ })
172
+ if (uploaded.commitSha !== pin.commitSha || uploaded.descriptorSha256 !== pin.descriptor.sha256) {
173
+ throw new Error('Proxy source upload identity mismatch')
174
+ }
175
+ const descriptor = prepareZeropsDescriptor(pin.descriptor.contents, 'proxy')
176
+ // Once triggered, only a lost lease stops the build request; a cancelled deploy leaves it to reconcile.
177
+ const buildSignal = followLeaseLoss(input.signal)
178
+ for (let attempt = 1;; attempt++) {
179
+ await input.boundary()
180
+ build = { ...build, triggered: true }
181
+ await save({ ...target, proxyBuild: build })
182
+ buildSignal.throwIfAborted()
183
+ try {
184
+ const process = await input.api.buildAndDeployAppVersion({
185
+ appVersionId: reserved.id,
186
+ zeropsYaml: descriptor.contents,
187
+ zeropsYamlSetup: 'proxy',
188
+ signal: buildSignal,
189
+ })
190
+ if (process.appVersionId !== undefined && process.appVersionId !== reserved.id) throw unresolved(reserved.id)
191
+ accepted = true
192
+ break
193
+ } catch (cause) {
194
+ buildSignal.throwIfAborted()
195
+ if (!(cause instanceof ZeropsApiError && cause.status === 400 && cause.code === 'userDataSyncRunning')) break
196
+ // A definitive refusal: the version is inert again, so a failure from here deletes it.
197
+ build = { ...build, triggered: false }
198
+ await save({ ...target, proxyBuild: build })
199
+ if (attempt >= USER_DATA_SYNC_ATTEMPTS) {
200
+ throw new ProviderNamespaceError(`Proxy version ${reserved.id}: service variables are still synchronizing`, 'userDataSyncRunning', true)
201
+ }
202
+ await input.sleep(POLL_INTERVAL_MS, input.signal)
203
+ }
204
+ }
205
+ } catch (cause) {
206
+ if (!build.triggered && !isLeaseLoss(input.signal.reason) && !isLeaseLoss(cause)) {
207
+ const cleanup = AbortSignal.any([followLeaseLoss(input.signal), AbortSignal.timeout(30_000)])
208
+ try {
209
+ await deleteVersion(input.api, reserved.id, cleanup)
210
+ await save({ ...target, proxyBuild: undefined })
211
+ } catch {
212
+ // The recorded attempt is deleted by the next owner.
213
+ }
214
+ }
215
+ throw cause
216
+ }
217
+ if (input.wait === 'started' && accepted) return target
218
+ await settle(build)
219
+ return target
220
+ }
@@ -0,0 +1,22 @@
1
+ import { z } from 'zod'
2
+ import { prepareZeropsDescriptor } from '../descriptor'
3
+ import { verifyZeropsArtifactSourceDescriptor, zeropsArtifactSourceDescriptorSchema } from '../manifest'
4
+ import { commitShaSchema, parseSourceWire, repositorySchema } from '../source/validation'
5
+ import type { ZeropsNamespaceProxySource } from './types'
6
+
7
+ export const namespaceProxySourceSchema = z.object({
8
+ repository: repositorySchema,
9
+ commitSha: commitShaSchema,
10
+ descriptor: zeropsArtifactSourceDescriptorSchema,
11
+ }).strict().transform((source) => {
12
+ prepareZeropsDescriptor(source.descriptor.contents, 'proxy')
13
+ return source
14
+ })
15
+
16
+ export const decodeZeropsNamespaceProxySource = (value: unknown): ZeropsNamespaceProxySource =>
17
+ parseSourceWire(namespaceProxySourceSchema, value, 'Zerops namespace proxy source')
18
+
19
+ export const verifyZeropsNamespaceProxySource = async (source: ZeropsNamespaceProxySource): Promise<void> => {
20
+ const parsed = decodeZeropsNamespaceProxySource(source)
21
+ await verifyZeropsArtifactSourceDescriptor(parsed.descriptor)
22
+ }
@@ -0,0 +1,115 @@
1
+ import type { ProviderDeploymentNamespace } from '@fabrika/provider-contract'
2
+ import { compileImportYaml, compileProvisioningYaml } from '../compile'
3
+ import type { ZeropsServiceSpec, ZeropsSourceTarget } from '../types'
4
+ import { ZEROPS_NAMESPACE_POSTGRES_HOSTNAME, ZEROPS_NAMESPACE_PROXY_HOSTNAME } from './constants'
5
+ import { decodeZeropsNamespaceProxySource } from './proxy-source'
6
+ import type {
7
+ ZeropsNamespacePolicy,
8
+ ZeropsNamespacePostgres,
9
+ ZeropsNamespacePresetInput,
10
+ ZeropsNamespacePublicAccess,
11
+ ZeropsNamespaceTarget,
12
+ ZeropsNamespaceTopology,
13
+ } from './types'
14
+
15
+ export const PROXY_TYPE = 'alpine@3.21'
16
+
17
+ /**
18
+ * The namespace-owned PostgreSQL service: the cheapest shape that runs, on EVERY environment.
19
+ *
20
+ * A profile sets the FLOOR and the tuning preset, never the cap — `oltp-hobby` and `oltp-production`
21
+ * share the same 8-core / 48 GB / 250 GB ceiling and the same vertical autoscaler, so the production
22
+ * preset does not buy headroom, it buys an idle bill. `oltp-hobby` starts at one SHARED core and
23
+ * 0.25 GB and grows under load; the shape is proven by a live app with dozens of tables.
24
+ *
25
+ * Keying this off `env === 'prod'` is what this default used to do, and it was wrong: most apps are
26
+ * small on every environment they have, and a name is not a size. HA and a bigger floor stay
27
+ * available and become a DELIBERATE act — `--postgres-type` and `--postgres-profile` on the namespace
28
+ * command — because the failure mode of over-provisioning is silent and monthly, while the failure
29
+ * mode of under-provisioning is visible and fixable at any time.
30
+ *
31
+ * Omitting `profile` would not be neutral: an HA service silently gets `oltp-production` (two
32
+ * DEDICATED cores and 4 GB per container, three containers) and a single one `oltp-staging`. Both
33
+ * read back off a live service.
34
+ */
35
+ const defaultPostgres = (): ZeropsNamespacePostgres => ({ type: 'postgresql:single@18', profile: 'oltp-hobby' })
36
+
37
+ /** A preset's project and service policy, without the proxy source pin (ADR-0045). */
38
+ export const zeropsNamespacePolicy = (input: Omit<ZeropsNamespacePresetInput, 'proxySource'>): ZeropsNamespacePolicy => ({
39
+ projectName: input.projectName,
40
+ // `LIGHT` on every environment. An APP namespace is not the platform project — the argument for
41
+ // `SERIOUS` in the platform installation topology is that the platform repairs the apps project, which no app
42
+ // namespace does. `corePackage` is upgrade-only, so this stays the caller's explicit choice.
43
+ corePackage: input.corePackage ?? 'LIGHT',
44
+ publicAccess: input.publicAccess ?? 'custom-domain',
45
+ managed: true,
46
+ ready: false,
47
+ ...(input.preset === 'cheap' ? { postgres: input.postgres ?? defaultPostgres() } : {}),
48
+ })
49
+
50
+ export const zeropsNamespacePreset = (input: ZeropsNamespacePresetInput): ZeropsNamespaceTarget => ({
51
+ proxySource: decodeZeropsNamespaceProxySource(input.proxySource),
52
+ ...zeropsNamespacePolicy(input),
53
+ })
54
+
55
+ export const projectMarker = (namespace: ProviderDeploymentNamespace): string => `Managed by Fabrika namespace ${namespace.id} (${namespace.env}).`
56
+
57
+ // Imports provision services only. The reserved upload-backed build selects the proxy setup.
58
+ //
59
+ // `enableSubdomainAccess` is a DECLARATION here and not a mechanism: the platform accepts it and drops it,
60
+ // including on the service this document creates. `ensureSubdomainAccess` below is what establishes the
61
+ // subdomain, after the proxy's first deploy. Written anyway because it is what ADR-0022's
62
+ // `assertOnlyPublicService` reads to prove no other service is claimed public.
63
+ const proxyService = (publicAccess: ZeropsNamespacePublicAccess): ZeropsServiceSpec => ({
64
+ hostname: ZEROPS_NAMESPACE_PROXY_HOSTNAME,
65
+ type: PROXY_TYPE,
66
+ priority: 10,
67
+ enableSubdomainAccess: publicAccess === 'zerops-subdomain',
68
+ // One container is a real availability trade — the proxy is the only enforcement point (ADR-0022),
69
+ // so a restart is a short outage rather than a rolling one — taken because it is a whole core of
70
+ // idle floor on every namespace, and most namespaces hold one small app. `maxContainers` is
71
+ // unchanged, so load still scales out.
72
+ minContainers: 1,
73
+ maxContainers: 6,
74
+ })
75
+
76
+ const postgresService = (postgres: ZeropsNamespacePostgres): ZeropsServiceSpec => ({
77
+ hostname: ZEROPS_NAMESPACE_POSTGRES_HOSTNAME,
78
+ type: postgres.type,
79
+ priority: 100,
80
+ ...(postgres.profile !== undefined ? { profile: postgres.profile } : {}),
81
+ })
82
+
83
+ /** Compile the one project and its namespace-owned services without depending on private deploy code. */
84
+ export const compileZeropsNamespaceTopology = (
85
+ namespace: ProviderDeploymentNamespace,
86
+ target: ZeropsNamespacePolicy,
87
+ ): ZeropsNamespaceTopology => {
88
+ const projectName = target.projectName
89
+ const corePackage = target.corePackage
90
+ const publicAccess = target.publicAccess
91
+ if (projectName === undefined || corePackage === undefined || publicAccess === undefined) {
92
+ throw new Error('Zerops namespace topology requires normalized project policy')
93
+ }
94
+ const services = (): ZeropsServiceSpec[] => [
95
+ ...(target.postgres === undefined ? [] : [postgresService(target.postgres)]),
96
+ proxyService(publicAccess),
97
+ ]
98
+ const source: ZeropsSourceTarget = {
99
+ platform: 'zerops',
100
+ project: {
101
+ name: projectName,
102
+ description: projectMarker(namespace),
103
+ corePackage,
104
+ tags: ['fabrika', 'namespace', namespace.id, namespace.env],
105
+ },
106
+ services,
107
+ }
108
+ const serviceSource: ZeropsSourceTarget = { platform: 'zerops', services }
109
+ return {
110
+ source,
111
+ createYaml: compileImportYaml({ target: source, ctx: { env: namespace.env } }).yaml,
112
+ servicesProvisionYaml: compileProvisioningYaml({ target: serviceSource, ctx: { env: namespace.env } }).yaml,
113
+ servicesSteadyYaml: compileImportYaml({ target: serviceSource, ctx: { env: namespace.env } }).yaml,
114
+ }
115
+ }
@@ -0,0 +1,114 @@
1
+ import type { ProviderNamespaceHost } from '@fabrika/provider-contract'
2
+ import type { ZeropsApi, ZeropsProjectMode } from '../api'
3
+ import type { Sleeper } from '../collaborators'
4
+ import type { ZeropsArtifactSourceDescriptor } from '../manifest'
5
+ import type { ZeropsSourceRepository } from '../source'
6
+ import type { ZeropsSourcePort } from '../source/port'
7
+ import type { ZeropsSourceTarget } from '../types'
8
+
9
+ export type ZeropsNamespacePublicAccess = 'custom-domain' | 'zerops-subdomain'
10
+
11
+ export interface ZeropsNamespaceProxySource {
12
+ readonly repository: ZeropsSourceRepository
13
+ readonly commitSha: string
14
+ readonly descriptor: ZeropsArtifactSourceDescriptor
15
+ }
16
+
17
+ /** The proxy attempt in flight (ADR-0044): a version not `triggered` is deleted, a triggered one is polled. */
18
+ export interface ZeropsNamespaceProxyBuild {
19
+ readonly appVersionId: string
20
+ readonly triggered: boolean
21
+ readonly manifestSha256?: string
22
+ }
23
+
24
+ export interface ZeropsNamespaceProxyDeployment {
25
+ readonly manifestSha256?: string
26
+ readonly appVersionId: string
27
+ readonly repository: ZeropsSourceRepository
28
+ readonly commitSha: string
29
+ readonly descriptorSha256: string
30
+ }
31
+
32
+ export type ZeropsNamespacePostgres =
33
+ | {
34
+ readonly type: 'postgresql:ha@18'
35
+ readonly profile?: 'oltp-staging' | 'oltp-production' | 'oltp-enterprise' | 'olap-production' | 'writeheavy-production'
36
+ }
37
+ | {
38
+ readonly type: 'postgresql:single@18'
39
+ readonly profile?: 'oltp-hobby' | 'oltp-staging' | 'oltp-production' | 'olap-production' | 'writeheavy-production'
40
+ }
41
+
42
+ /**
43
+ * Provider-owned namespace state. Optional fields are durable progress checkpoints; values that are
44
+ * absent are resolved by `normalize()` before a mutation starts.
45
+ */
46
+ export interface ZeropsNamespaceTarget {
47
+ readonly proxySource: ZeropsNamespaceProxySource
48
+ readonly proxyBuild?: ZeropsNamespaceProxyBuild
49
+ readonly proxyDeployment?: ZeropsNamespaceProxyDeployment
50
+ readonly projectName?: string
51
+ readonly corePackage?: ZeropsProjectMode
52
+ readonly publicAccess?: ZeropsNamespacePublicAccess
53
+ readonly postgres?: ZeropsNamespacePostgres
54
+ readonly managed?: boolean
55
+ readonly projectId?: string
56
+ readonly proxyServiceId?: string
57
+ readonly postgresServiceId?: string
58
+ readonly proxyConfigured?: boolean
59
+ readonly proxyBaselineSequence?: number
60
+ readonly proxyAppVersionId?: string
61
+ /** The hosts the published proxy answers on, recorded once so a namespace read needs no platform call. */
62
+ readonly proxyHosts?: readonly ProviderNamespaceHost[]
63
+ readonly ready?: boolean
64
+ }
65
+
66
+ export type ZeropsNamespacePolicy = Omit<ZeropsNamespaceTarget, 'proxySource'>
67
+
68
+ export interface NormalizedZeropsNamespaceTarget extends ZeropsNamespaceTarget {
69
+ readonly projectName: string
70
+ readonly corePackage: ZeropsProjectMode
71
+ readonly publicAccess: ZeropsNamespacePublicAccess
72
+ readonly managed: boolean
73
+ readonly ready: boolean
74
+ }
75
+
76
+ export interface ZeropsNamespaceOptions {
77
+ readonly proxySource: ZeropsNamespaceProxySource
78
+ readonly source?: ZeropsSourcePort
79
+ readonly clientId: string
80
+ /** Public IAM origin reachable from an app project. */
81
+ readonly iamUrl: string
82
+ /** Same credential IAM receives as `FABRIKA_IAM_PROXY_KEY`. */
83
+ readonly iamKey: string
84
+ readonly api: ZeropsApi
85
+ readonly sleep?: Sleeper
86
+ }
87
+
88
+ export interface ZeropsNamespaceTopology {
89
+ readonly source: ZeropsSourceTarget
90
+ readonly createYaml: string
91
+ readonly servicesProvisionYaml: string
92
+ readonly servicesSteadyYaml: string
93
+ }
94
+
95
+ export interface ZeropsNamespacePresetInput {
96
+ readonly proxySource: ZeropsNamespaceProxySource
97
+ readonly preset: 'cheap' | 'mid' | 'full'
98
+ readonly env: string
99
+ readonly projectName: string
100
+ readonly corePackage?: ZeropsProjectMode
101
+ readonly publicAccess?: ZeropsNamespacePublicAccess
102
+ readonly postgres?: ZeropsNamespacePostgres
103
+ }
104
+
105
+ export interface ZeropsNamespaceOperatorOptions {
106
+ readonly proxySource: ZeropsNamespaceProxySource
107
+ }
108
+
109
+ export interface ZeropsNamespacePlanOptions {
110
+ readonly projectName?: string
111
+ readonly corePackage?: ZeropsProjectMode
112
+ readonly publicAccess?: ZeropsNamespacePublicAccess
113
+ readonly postgres?: ZeropsNamespacePostgres
114
+ }