@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fabrika/provider-zerops",
3
- "version": "0.8.1",
3
+ "version": "0.9.0-cloudflare.1",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -12,27 +12,28 @@
12
12
  },
13
13
  "files": [
14
14
  "src",
15
- "!src/**/__tests__/**"
15
+ "!src/**/__tests__/**",
16
+ "types"
16
17
  ],
17
18
  "exports": {
18
19
  ".": {
19
20
  "bun": "./src/index.ts",
20
- "types": "./src/index.ts",
21
+ "types": "./types/src/index.d.ts",
21
22
  "default": "./src/index.ts"
22
23
  },
23
24
  "./cli": {
24
25
  "bun": "./src/cli.ts",
25
- "types": "./src/cli.ts",
26
+ "types": "./types/src/cli.d.ts",
26
27
  "default": "./src/cli.ts"
27
28
  },
28
29
  "./installation": {
29
30
  "bun": "./src/installation/index.ts",
30
- "types": "./src/installation/index.ts",
31
+ "types": "./types/src/installation/index.d.ts",
31
32
  "default": "./src/installation/index.ts"
32
33
  },
33
34
  "./installation/topology": {
34
35
  "bun": "./src/installation/topology/index.ts",
35
- "types": "./src/installation/topology/index.ts",
36
+ "types": "./types/src/installation/topology/index.d.ts",
36
37
  "default": "./src/installation/topology/index.ts"
37
38
  }
38
39
  },
@@ -43,14 +44,15 @@
43
44
  },
44
45
  "dependencies": {
45
46
  "zod": "^3.25.76",
46
- "@fabrika/auth": "0.8.1",
47
- "@fabrika/auth-core": "0.8.1",
48
- "@fabrika/email": "0.8.1",
49
- "@fabrika/iam-contract": "0.8.1",
50
- "@fabrika/installation-init": "0.8.1",
51
- "@fabrika/operations-contract": "0.8.1",
52
- "@fabrika/provider-contract": "0.8.1",
53
- "@fabrika/proxy-core": "0.8.1",
47
+ "@fabrika/app": "0.9.0-cloudflare.1",
48
+ "@fabrika/auth": "0.9.0-cloudflare.1",
49
+ "@fabrika/auth-core": "0.9.0-cloudflare.1",
50
+ "@fabrika/email": "0.9.0-cloudflare.1",
51
+ "@fabrika/iam-contract": "0.9.0-cloudflare.1",
52
+ "@fabrika/installation-init": "0.9.0-cloudflare.1",
53
+ "@fabrika/operations-contract": "0.9.0-cloudflare.1",
54
+ "@fabrika/provider-contract": "0.9.0-cloudflare.1",
55
+ "@fabrika/proxy-core": "0.9.0-cloudflare.1",
54
56
  "yaml": "^2.9.1"
55
57
  }
56
58
  }
package/src/api/client.ts CHANGED
@@ -5,6 +5,7 @@ import { waitForProcess } from './polling'
5
5
  import {
6
6
  appVersionListSchema,
7
7
  appVersionSchema,
8
+ backupConfigSchema,
8
9
  importResultSchema,
9
10
  integrationTokenSchema,
10
11
  logAccessSchema,
@@ -346,6 +347,12 @@ export const createZeropsApi = (options: ZeropsApiOptions): ZeropsApi => {
346
347
  await request('enable subdomain access', 'PUT', `/service-stack/${serviceId}/enable-subdomain-access`, { signal })
347
348
  },
348
349
 
350
+ startService: async ({ serviceId, signal }) =>
351
+ readResponse('start service', processSchema, await request('start service', 'PUT', `/service-stack/${serviceId}/start`, { signal })),
352
+
353
+ stopService: async ({ serviceId, signal }) =>
354
+ readResponse('stop service', processSchema, await request('stop service', 'PUT', `/service-stack/${serviceId}/stop`, { signal })),
355
+
349
356
  configureAutoscaling: async ({ serviceId, autoscalingProfileId, signal }) => {
350
357
  const payload = await request('configure autoscaling', 'PUT', `/service-stack/${serviceId}/autoscaling`, {
351
358
  body: { autoscalingProfileId },
@@ -364,6 +371,17 @@ export const createZeropsApi = (options: ZeropsApiOptions): ZeropsApi => {
364
371
  return readResponse('configure custom autoscaling', processNilSchema, payload).process ?? null
365
372
  },
366
373
 
374
+ getBackupConfig: async ({ serviceId, signal }) =>
375
+ readResponse('list backups', backupConfigSchema, await request('list backups', 'GET', `/service-stack/${serviceId}/backup`, { signal })),
376
+
377
+ configureBackup: async ({ serviceId, backupPeriod, retentionPolicy, signal }) => {
378
+ const payload = await request('configure backup', 'PUT', `/service-stack/${serviceId}/backup-config`, {
379
+ body: { backupPeriod, retentionPolicy },
380
+ signal,
381
+ })
382
+ return readResponse('configure backup', processNilSchema, payload).process ?? null
383
+ },
384
+
367
385
  getService: async ({ serviceId, signal }) =>
368
386
  readResponse('get service', serviceSchema, await request('get service', 'GET', `/service-stack/${serviceId}`, { signal })),
369
387
 
@@ -3,6 +3,8 @@ import { withoutSensitiveContent } from './service-env-response'
3
3
  import type {
4
4
  ZeropsAppVersion,
5
5
  ZeropsAppVersionStatus,
6
+ ZeropsBackupConfig,
7
+ ZeropsBackupRetentionPolicy,
6
8
  ZeropsImportResult,
7
9
  ZeropsIntegrationToken,
8
10
  ZeropsLogAccess,
@@ -170,6 +172,33 @@ export const serviceSchema = z.object({
170
172
  currentAutoscaling: optional(serviceAutoscalingSchema),
171
173
  }).transform(({ activeAppVersion, ...service }): ZeropsService => ({ ...service, activeAppVersionId: activeAppVersion?.id }))
172
174
 
175
+ const backupCountSchema = z.number().int().nonnegative()
176
+
177
+ /** Strict like the custom autoscaling, for the same reason: it is sent back whole, so no field may read as absent. */
178
+ const backupRetentionPolicySchema = z.object({
179
+ maxTotalFiles: backupCountSchema,
180
+ maxTotalGiB: backupCountSchema,
181
+ // A Go API may serialise an empty list as null; read as the empty list it is.
182
+ protectedTags: z.array(z.string()).nullable().transform((tags) => tags ?? []),
183
+ minDaily: backupCountSchema,
184
+ maxDaily: backupCountSchema,
185
+ minWeekly: backupCountSchema,
186
+ maxWeekly: backupCountSchema,
187
+ minMonthly: backupCountSchema,
188
+ maxMonthly: backupCountSchema,
189
+ }) satisfies z.ZodType<ZeropsBackupRetentionPolicy, z.ZodTypeDef, unknown>
190
+
191
+ /** A service's own retention that does not parse fails the whole read: reading it as absent would send the default over it. */
192
+ export const backupConfigSchema = z.object({
193
+ backupPeriod: z.string(),
194
+ defaultRetentionPolicy: backupRetentionPolicySchema,
195
+ retentionPolicy: backupRetentionPolicySchema.nullish(),
196
+ }).transform(({ backupPeriod, defaultRetentionPolicy, retentionPolicy }): ZeropsBackupConfig => ({
197
+ backupPeriod,
198
+ defaultRetentionPolicy,
199
+ ...(retentionPolicy === null || retentionPolicy === undefined ? {} : { retentionPolicy }),
200
+ }))
201
+
173
202
  export const projectSchema = z.object({
174
203
  id: idSchema,
175
204
  name: z.string(),
package/src/api/types.ts CHANGED
@@ -196,6 +196,34 @@ export interface ZeropsServiceAutoscaling {
196
196
  }
197
197
  }
198
198
 
199
+ /**
200
+ * How many backups of a managed service the platform keeps. VERIFIED AS A SHAPE: `OutDtoBackupRetentionPolicy` and
201
+ * `InDtoBodyBackupRetentionPolicy` carry these nine fields, all required. The `min*`/`max*` counts are per backup TAG
202
+ * (`daily`, `weekly`, `monthly`); UNVERIFIED: which tags an hourly automatic backup gets.
203
+ */
204
+ export interface ZeropsBackupRetentionPolicy {
205
+ maxTotalFiles: number
206
+ maxTotalGiB: number
207
+ /** Backups with one of these tags are never deleted automatically. */
208
+ protectedTags: string[]
209
+ minDaily: number
210
+ maxDaily: number
211
+ minWeekly: number
212
+ maxWeekly: number
213
+ minMonthly: number
214
+ maxMonthly: number
215
+ }
216
+
217
+ /** A managed service's backup schedule and retention. VERIFIED AS A SHAPE: `ResponseServiceStackBackupFileList`, without its `files`. */
218
+ export interface ZeropsBackupConfig {
219
+ /** UNVERIFIED: the format. Zerops' GUI documentation describes a 5-field cron (`min hour dom mon dow`). */
220
+ backupPeriod: string
221
+ /** What the platform applies to a service with no retention of its own. UNVERIFIED: that it is what applies then. */
222
+ defaultRetentionPolicy: ZeropsBackupRetentionPolicy
223
+ /** The service's own retention; absent when none was set. */
224
+ retentionPolicy?: ZeropsBackupRetentionPolicy
225
+ }
226
+
199
227
  /** One service. VERIFIED: `ResponseServiceStack` (narrowed to the fields fabrika reads). */
200
228
  export interface ZeropsService {
201
229
  id: string
@@ -472,6 +500,18 @@ export interface ZeropsApi {
472
500
  */
473
501
  enableSubdomainAccess(input: { serviceId: string; signal: AbortSignal }): Promise<void>
474
502
 
503
+ // ── service-stack: running state ──────────────────────────────────────────────
504
+
505
+ /**
506
+ * Start a stopped service. VERIFIED AS A SHAPE: `PUT /service-stack/{id}/start` (`StartServiceStack`), no body,
507
+ * `ResponseProcess`. Measured 2026-10-21: a stopped Bun service served again about 9 s after the call. Files written
508
+ * at runtime survive a stop and a start; a deploy replaces the container and loses them.
509
+ */
510
+ startService(input: { serviceId: string; signal: AbortSignal }): Promise<ZeropsProcess>
511
+
512
+ /** Stop a service. VERIFIED AS A SHAPE: `PUT /service-stack/{id}/stop` (`StopServiceStack`), no body, `ResponseProcess`. */
513
+ stopService(input: { serviceId: string; signal: AbortSignal }): Promise<ZeropsProcess>
514
+
475
515
  // ── service-stack: autoscaling ────────────────────────────────────────────────
476
516
 
477
517
  /**
@@ -494,6 +534,30 @@ export interface ZeropsApi {
494
534
  */
495
535
  configureCustomAutoscaling(input: { serviceId: string; autoscaling: ZeropsServiceAutoscaling; signal: AbortSignal }): Promise<ZeropsProcess | null>
496
536
 
537
+ // ── service-stack: backups ────────────────────────────────────────────────────
538
+
539
+ /**
540
+ * A managed service's backup schedule and retention. VERIFIED AS A SHAPE: `GET /service-stack/{id}/backup`
541
+ * (`ListBackups`), response `ResponseServiceStackBackupFileList`; the backup files it also lists are not read.
542
+ */
543
+ getBackupConfig(input: { serviceId: string; signal: AbortSignal }): Promise<ZeropsBackupConfig>
544
+
545
+ /**
546
+ * Set a managed service's backup schedule and retention. VERIFIED AS A SHAPE: `PUT /service-stack/{id}/backup-config`
547
+ * (`ConfigureBackup`), body `RequestServiceStackBackupConfig`, response `ResponseProcessNil`. The platform stores the
548
+ * period as the service's `BACKUP_PERIOD` variable and may start a user-data sync, whose process this answers.
549
+ *
550
+ * `retentionPolicy` is required here although the body makes it optional. UNVERIFIED: whether a request without one
551
+ * resets the service's retention; a PUT elsewhere on this API resets every field it leaves out, so the caller sends
552
+ * the retention it read, whole.
553
+ */
554
+ configureBackup(input: {
555
+ serviceId: string
556
+ backupPeriod: string
557
+ retentionPolicy: ZeropsBackupRetentionPolicy
558
+ signal: AbortSignal
559
+ }): Promise<ZeropsProcess | null>
560
+
497
561
  // ── service-stack / project: reading state ────────────────────────────────────
498
562
 
499
563
  /** One service by id. VERIFIED: `GET /service-stack/{id}`. */
package/src/api.ts CHANGED
@@ -19,6 +19,8 @@ export {
19
19
  type ZeropsAppVersion,
20
20
  type ZeropsAppVersionStatus,
21
21
  type ZeropsAppVersionUpload,
22
+ type ZeropsBackupConfig,
23
+ type ZeropsBackupRetentionPolicy,
22
24
  type ZeropsImportedService,
23
25
  type ZeropsImportResult,
24
26
  type ZeropsIntegrationToken,
package/src/cli.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env bun
2
+ import type { AppCli } from '@fabrika/provider-contract/app-cli'
2
3
  import { constants } from 'node:fs'
3
4
  import { open } from 'node:fs/promises'
4
5
  import { resolve } from 'node:path'
@@ -12,6 +13,7 @@ import {
12
13
  manifestServiceHostnames,
13
14
  ZEROPS_SOURCE_DESCRIPTOR_MAX_BYTES,
14
15
  } from './manifest'
16
+ import { zeropsNamespaceCli } from './namespace-cli'
15
17
  import type { ZeropsAppConfig } from './types'
16
18
 
17
19
  const USAGE = `Zerops app tools
@@ -106,6 +108,15 @@ export const runZeropsCli = async (argv: readonly string[]): Promise<void> => {
106
108
  console.info(`wrote ${output} (${manifestServiceHostnames(manifest).length} service(s))`)
107
109
  }
108
110
 
111
+ /** What `fabrika app build`, `app register` and `fabrika namespace` run for an application on Zerops. */
112
+ export const appCli: AppCli = {
113
+ provider: 'zerops',
114
+ entryRuntime: 'bun',
115
+ build: (argv) => runZeropsCli(['build', ...argv]),
116
+ registrationManifest: buildZeropsManifestText,
117
+ namespaces: zeropsNamespaceCli,
118
+ }
119
+
109
120
  if (import.meta.main) {
110
121
  await runZeropsCli(process.argv.slice(2)).catch((error: unknown) => {
111
122
  console.error(error instanceof Error ? error.message : String(error))
@@ -1,5 +1,3 @@
1
- import { reconcileSchema } from '@fabrika/auth'
2
- import type { SchemaReconciler } from '@fabrika/provider-contract'
3
1
  import { createZeropsApi, type ZeropsApi } from './api'
4
2
  import { type ExposureAlerter, sendExposureAlert } from './exposure'
5
3
  import type { ZeropsSourcePort } from './source/port'
@@ -28,7 +26,6 @@ export interface ZeropsCollaborators {
28
26
  api: ZeropsApi
29
27
  /** Injected by the Zerops control composition; dry runs do not require it. */
30
28
  source?: ZeropsSourcePort
31
- reconcileSchema: SchemaReconciler
32
29
  /** Raises the Operations alert for a refused activation; best-effort. */
33
30
  alertExposure: ExposureAlerter
34
31
  sleep: Sleeper
@@ -36,19 +33,8 @@ export interface ZeropsCollaborators {
36
33
 
37
34
  export type ZeropsCollaboratorFactory = (target: ZeropsRuntimeTarget) => ZeropsCollaborators
38
35
 
39
- const defaultReconcileSchema: SchemaReconciler = (input) =>
40
- reconcileSchema({
41
- url: input.url,
42
- app: input.app,
43
- schema: input.schema,
44
- ...(input.returnOrigins === undefined ? {} : { returnOrigins: input.returnOrigins }),
45
- adminKey: input.adminKey,
46
- signal: input.signal,
47
- })
48
-
49
36
  export const defaultZeropsCollaborators: ZeropsCollaboratorFactory = (target) => ({
50
37
  api: createZeropsApi({ token: target.accessToken, baseUrl: target.apiBaseUrl }),
51
- reconcileSchema: defaultReconcileSchema,
52
38
  alertExposure: sendExposureAlert,
53
39
  sleep: defaultSleep,
54
40
  })
package/src/control.ts CHANGED
@@ -1,4 +1,3 @@
1
- import { appEnvironmentId } from '@fabrika/auth-core'
2
1
  import type {
3
2
  ControlProvider,
4
3
  JsonValue,
@@ -8,6 +7,7 @@ import type {
8
7
  ProviderDeployInput,
9
8
  ProviderDeploymentNamespace,
10
9
  ProviderDeployOutcome,
10
+ ProviderDevEnvironmentInput,
11
11
  ProviderEnvelope,
12
12
  ProviderEnvironment,
13
13
  ProviderReconcileOutcome,
@@ -16,7 +16,6 @@ import type {
16
16
  ProviderSourceBinding,
17
17
  ProviderSourceResolution,
18
18
  ProviderSourceResolutionInput,
19
- SchemaReconciler,
20
19
  } from '@fabrika/provider-contract'
21
20
  import { decodeEnvelope, ProviderSecretError } from '@fabrika/provider-contract'
22
21
  import { z } from 'zod'
@@ -31,9 +30,8 @@ import {
31
30
  } from './api'
32
31
  import { defaultSleep, defaultZeropsCollaborators, type Sleeper } from './collaborators'
33
32
  import { createdServices, type ExposureAlerter, isObjectStorageType } from './exposure'
34
- import { assertAuthoredManagedInputs, assertAuthoredManagedName, assertNoFrameworkReference, parseManagedEnvironment } from './managed-env'
33
+ import { assertAuthoredManagedInputs, assertAuthoredManagedName, assertNoFrameworkReference } from './managed-env'
35
34
  import {
36
- approvedSchemaOf,
37
35
  assertFabrikaManifestCoordinates,
38
36
  type FabrikaManifest,
39
37
  manifestServiceHostnames,
@@ -90,11 +88,8 @@ const envelope = <T>(codec: ProviderCodec<T>, value: T): ProviderEnvelope => ({
90
88
  export interface ZeropsControlProviderOptions {
91
89
  readonly accessToken: string
92
90
  readonly apiBaseUrl?: string
93
- readonly propustkaUrl?: string
94
- readonly adminKey?: string
95
91
  readonly api?: ZeropsApi
96
92
  readonly source: ZeropsSourcePort
97
- readonly reconcileSchema?: SchemaReconciler
98
93
  readonly alertExposure?: ExposureAlerter
99
94
  readonly sleep?: Sleeper
100
95
  readonly namespaces?: {
@@ -102,6 +97,8 @@ export interface ZeropsControlProviderOptions {
102
97
  readonly proxySource: ZeropsNamespaceProxySource
103
98
  readonly iamUrl: string
104
99
  readonly iamKey: string
100
+ /** The installation's own project, which no namespace may adopt. */
101
+ readonly installationProjectId?: string
105
102
  /** The manifest parsers of the older releases' proxies, which a namespace pinned to an older source may run. */
106
103
  readonly olderProxies: OlderProxyReaders
107
104
  }
@@ -226,7 +223,6 @@ export const createZeropsControlProvider = (options: ZeropsControlProviderOption
226
223
  return {
227
224
  api,
228
225
  source: options.source,
229
- reconcileSchema: options.reconcileSchema ?? defaults.reconcileSchema,
230
226
  alertExposure: options.alertExposure ?? defaults.alertExposure,
231
227
  sleep: options.sleep ?? defaultSleep,
232
228
  }
@@ -260,34 +256,6 @@ export const createZeropsControlProvider = (options: ZeropsControlProviderOption
260
256
  }
261
257
  }
262
258
 
263
- const reconcileActiveSchema = async (
264
- input: { environment: ProviderEnvironment; returnOrigins?: readonly string[] },
265
- signal: AbortSignal,
266
- ): Promise<void> => {
267
- const artifact = artifactFor(input.environment)
268
- const schema = approvedSchemaOf(input.environment)
269
- if (schema === undefined || options.propustkaUrl === undefined) return
270
- const placement = resolvedEnvironment(input.environment, { requireReady: false })
271
- const runtimeTarget: ZeropsRuntimeTarget = {
272
- projectId: placement.projectId,
273
- serviceId: placement.target.serviceId,
274
- accessToken: options.accessToken,
275
- ...(options.apiBaseUrl !== undefined ? { apiBaseUrl: options.apiBaseUrl } : {}),
276
- propustkaUrl: options.propustkaUrl,
277
- ...(options.adminKey !== undefined ? { adminKey: options.adminKey } : {}),
278
- }
279
- const reconcileSchema = options.reconcileSchema ?? defaultZeropsCollaborators(runtimeTarget).reconcileSchema
280
- signal.throwIfAborted()
281
- await reconcileSchema({
282
- url: options.propustkaUrl,
283
- app: appEnvironmentId(artifact.app.id, artifact.app.env),
284
- schema,
285
- ...(input.returnOrigins === undefined || input.returnOrigins.length === 0 ? {} : { returnOrigins: input.returnOrigins }),
286
- adminKey: options.adminKey,
287
- signal,
288
- })
289
- }
290
-
291
259
  const assertVersionCoordinates = (environment: ProviderEnvironment, appVersionId: string, version: ZeropsAppVersion): void => {
292
260
  const serviceId = decodeEnvironment(environment).target.serviceId
293
261
  if (version.id !== appVersionId || (version.serviceStackId !== undefined && version.serviceStackId !== serviceId)) {
@@ -322,9 +290,31 @@ export const createZeropsControlProvider = (options: ZeropsControlProviderOption
322
290
  return { commitSha: result.commitSha }
323
291
  }
324
292
 
293
+ /** Stop or start the service of a `dev` environment outside a run, and wait for the platform's process. */
294
+ const runningState = async (input: ProviderDevEnvironmentInput, action: 'start' | 'stop'): Promise<void> => {
295
+ if (input.environment.kind !== 'dev') throw new Error('zerops: only a dev environment is stopped and started outside a run')
296
+ const placement = resolvedEnvironment(input.environment, { requireReady: false })
297
+ const serviceId = placement.target.serviceId
298
+ // Idempotent: Control's record of a stopped service can lag behind a start from elsewhere.
299
+ const status = (await api.getService({ serviceId, signal: input.signal })).status
300
+ if (status === (action === 'stop' ? 'STOPPED' : 'ACTIVE')) return
301
+ const process = action === 'stop'
302
+ ? await api.stopService({ serviceId, signal: input.signal })
303
+ : await api.startService({ serviceId, signal: input.signal })
304
+ await waitForProcess({
305
+ api,
306
+ processId: process.id,
307
+ sleep: options.sleep ?? defaultSleep,
308
+ signal: input.signal,
309
+ label: `the ${action} of ${input.environment.appId}/${input.environment.env}`,
310
+ })
311
+ }
312
+
325
313
  const controlProvider: ControlProvider = {
326
314
  id: 'zerops',
327
315
  normalizeRegistration,
316
+ // The deploy reads `zerops.yaml` from the repository, so a workspace may not change it (ADR-0049 D6).
317
+ authoring: { protectedFiles: ['zerops.yaml', 'zerops.yml'] },
328
318
  source: { binding: 'connection' },
329
319
  /**
330
320
  * A Zerops artifact IS the compiled manifest, so the declaration is right here and core can refuse
@@ -347,8 +337,6 @@ export const createZeropsControlProvider = (options: ZeropsControlProviderOption
347
337
  deploy: async (input: ProviderDeployInput): Promise<ProviderDeployOutcome> => {
348
338
  input.signal.throwIfAborted()
349
339
  assertAuthoredManagedInputs({ variableNames: Object.keys(input.vars), secretNames: Object.keys(input.secrets) })
350
- const handed = parseManagedEnvironment(input.managedEnvironment)
351
- const managedEnvironment = { ...handed.values, ...handed.familyVariables }
352
340
  const binding = privateBinding(input.sourceBinding)
353
341
  assertEnvironmentOfApp(input.app, input.environment)
354
342
  const placement = resolvedEnvironment(input.environment, { requireReady: true })
@@ -374,16 +362,14 @@ export const createZeropsControlProvider = (options: ZeropsControlProviderOption
374
362
  ...(placement.target.createdBuckets === undefined ? {} : { createdBuckets: placement.target.createdBuckets }),
375
363
  ...(source === undefined ? {} : { source }),
376
364
  ...(options.apiBaseUrl !== undefined ? { apiBaseUrl: options.apiBaseUrl } : {}),
377
- ...(options.propustkaUrl !== undefined ? { propustkaUrl: options.propustkaUrl } : {}),
378
- ...(options.adminKey !== undefined ? { adminKey: options.adminKey } : {}),
379
365
  }
380
366
  const result = await deployRun({
381
367
  appId: input.app.id,
382
368
  env: input.environment.env,
383
- ...(input.returnOrigins === undefined ? {} : { returnOrigins: input.returnOrigins }),
369
+ ...(input.environment.kind === 'dev' && source !== undefined ? { dev: { head: source.commitSha } } : {}),
384
370
  secrets: input.secrets,
385
371
  vars: input.vars,
386
- managedEnvironment,
372
+ managedEnvironment: input.managedEnvironment,
387
373
  target: runtimeTarget,
388
374
  artifact,
389
375
  ...(input.environment.access === undefined ? {} : { access: input.environment.access }),
@@ -395,6 +381,10 @@ export const createZeropsControlProvider = (options: ZeropsControlProviderOption
395
381
  input.signal.throwIfAborted()
396
382
  return { state: result.status, ...(result.artifactState === undefined ? {} : { artifactState: result.artifactState }) }
397
383
  },
384
+ devEnvironments: {
385
+ stop: (input) => runningState(input, 'stop'),
386
+ start: (input) => runningState(input, 'start'),
387
+ },
398
388
  cancel: async (input): Promise<ProviderCancelOutcome> => {
399
389
  const signal = input.signal
400
390
  signal.throwIfAborted()
@@ -412,17 +402,8 @@ export const createZeropsControlProvider = (options: ZeropsControlProviderOption
412
402
  const version = await getVersion({ appVersionId: checkpoint.appVersionId, signal })
413
403
  if (version === null) return { state: 'settled', outcome: 'failed' }
414
404
  assertVersionCoordinates(input.environment, checkpoint.appVersionId, version)
415
- // Cancellation never implies rollback: a version that activated first makes the run a success,
416
- // which finishes the same IAM touchpoint a deploy does.
417
- if (version.status === ZEROPS_ACTIVE) {
418
- try {
419
- await reconcileActiveSchema(input, signal)
420
- } catch {
421
- signal.throwIfAborted()
422
- return { state: 'pending' }
423
- }
424
- return { state: 'settled', outcome: 'succeeded' }
425
- }
405
+ // Cancellation never implies rollback: a version that activated first makes the run a success.
406
+ if (version.status === ZEROPS_ACTIVE) return { state: 'settled', outcome: 'succeeded' }
426
407
  if (version.status !== undefined && ZEROPS_TERMINAL.has(version.status)) return { state: 'settled', outcome: 'failed' }
427
408
  await cancelBuildQuietly(checkpoint.appVersionId, signal)
428
409
  // A build request that never landed leaves the version UPLOADING for good; the deadline settles it.
@@ -459,10 +440,7 @@ export const createZeropsControlProvider = (options: ZeropsControlProviderOption
459
440
  }
460
441
  if (version === null) return { state: 'failed' }
461
442
  assertVersionCoordinates(input.environment, checkpoint.appVersionId, version)
462
- if (version.status === ZEROPS_ACTIVE) {
463
- await reconcileActiveSchema(input, signal)
464
- return { state: 'succeeded' }
465
- }
443
+ if (version.status === ZEROPS_ACTIVE) return { state: 'succeeded' }
466
444
  if (version.status !== undefined && ZEROPS_TERMINAL.has(version.status)) return { state: 'failed' }
467
445
  if (pastDeadline(input.startedAtMs)) {
468
446
  await cancelBuildQuietly(checkpoint.appVersionId, signal)