@fabrika/provider-zerops 0.2.0 → 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 (82) 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 +3 -1
  14. package/src/control.ts +203 -450
  15. package/src/descriptor.ts +135 -0
  16. package/src/index.ts +23 -127
  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 +191 -344
  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 -1271
  71. package/src/plan.ts +21 -23
  72. package/src/provider.ts +266 -241
  73. package/src/source/constants.ts +11 -0
  74. package/src/source/port.ts +49 -0
  75. package/src/source/types.ts +4 -0
  76. package/src/source/validation.ts +72 -0
  77. package/src/source.ts +2 -1220
  78. package/src/types.ts +8 -7
  79. package/src/version-attempt.ts +18 -0
  80. package/src/codec.ts +0 -108
  81. package/src/namespace-command.ts +0 -209
  82. package/src/source-connection.ts +0 -344
@@ -1,344 +0,0 @@
1
- import type { ZeropsApi, ZeropsService, ZeropsServiceEnv } from './api'
2
- import {
3
- decodeZeropsSourceCredentialBundleV2,
4
- sha256ZeropsSourceCredentialBundleV2,
5
- ZEROPS_SOURCE_CREDENTIAL_BUNDLE_VERSION_V2,
6
- ZEROPS_SOURCE_PROTOCOL_VERSION_V2,
7
- type ZeropsSourceCredentialActivateResponseV2,
8
- zeropsSourceCredentialEnvV2,
9
- type ZeropsSourceCredentialManager,
10
- type ZeropsSourceCredentialManagerV2,
11
- type ZeropsSourceCredentialStatusResponseV2,
12
- type ZeropsSourceGitHubAppIdentityV1,
13
- type ZeropsSourceInstallationsVerifyInput,
14
- type ZeropsSourceInstallationsVerifyResponseV1,
15
- type ZeropsSourceWebhookConfigureInput,
16
- type ZeropsSourceWebhookConfigureResponseV1,
17
- } from './source'
18
-
19
- const SOURCE_HOSTNAME = 'source'
20
- const PROJECT_ID_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/
21
- const DEFAULT_REREAD_ATTEMPTS = 5
22
- const DEFAULT_REREAD_DELAY_MS = 250
23
-
24
- export type SourceConnectionInspection =
25
- | { readonly state: 'unavailable' }
26
- | { readonly state: 'anonymous' }
27
-
28
- export type SourceConnectionStatus =
29
- | SourceConnectionInspection
30
- | { readonly state: 'activation-required'; readonly credentialSha256: string }
31
- | {
32
- readonly state: 'active'
33
- readonly credentialSha256: string
34
- readonly githubApp: ZeropsSourceGitHubAppIdentityV1
35
- }
36
-
37
- export interface SourceConnectionActivateInput {
38
- readonly connectionId: string
39
- readonly credentialBundle: string
40
- readonly credentialSha256: string
41
- readonly signal: AbortSignal
42
- }
43
-
44
- export interface SourceConnectionStatusInput {
45
- readonly connectionId: string
46
- readonly signal: AbortSignal
47
- }
48
-
49
- /** Provider-neutral lifecycle consumed by the authenticated control connection flow. */
50
- export interface SourceConnectionAdmin {
51
- inspect(signal: AbortSignal): Promise<SourceConnectionInspection>
52
- activateV2(input: SourceConnectionActivateInput): Promise<ZeropsSourceCredentialActivateResponseV2>
53
- statusV2(input: SourceConnectionStatusInput): Promise<SourceConnectionStatus>
54
- configureWebhook(input: ZeropsSourceWebhookConfigureInput): Promise<ZeropsSourceWebhookConfigureResponseV1>
55
- verifyInstallations(input: ZeropsSourceInstallationsVerifyInput): Promise<ZeropsSourceInstallationsVerifyResponseV1>
56
- }
57
-
58
- export type SourceConnectionZeropsApi = Pick<ZeropsApi, 'findService' | 'listServiceEnv' | 'createServiceEnv'>
59
-
60
- export interface ZeropsSourceConnectionAdminOptions {
61
- readonly api: SourceConnectionZeropsApi
62
- readonly source: ZeropsSourceCredentialManager & ZeropsSourceCredentialManagerV2
63
- readonly projectId: string
64
- readonly reread?: {
65
- readonly attempts?: number
66
- readonly delayMs?: number
67
- readonly sleep?: (delayMs: number, signal: AbortSignal) => Promise<void>
68
- }
69
- }
70
-
71
- export type SourceConnectionAdminErrorCode =
72
- | 'invalid_configuration'
73
- | 'source_not_found'
74
- | 'source_mismatch'
75
- | 'credential_conflict'
76
- | 'credential_persistence'
77
- | 'credential_activation'
78
-
79
- /** Stable detail-free failure. It never carries a project id, service id, environment value, or upstream body. */
80
- export class SourceConnectionAdminError extends Error {
81
- constructor(readonly code: SourceConnectionAdminErrorCode) {
82
- super(`source connection administration failed (${code})`)
83
- this.name = 'SourceConnectionAdminError'
84
- }
85
- }
86
-
87
- interface DurableInspectionV2 {
88
- readonly public:
89
- | { readonly state: 'anonymous' }
90
- | { readonly state: 'durable'; readonly credentialSha256: string }
91
- readonly service: ZeropsService
92
- readonly environmentKey: string
93
- readonly bundle?: string
94
- }
95
-
96
- export function createZeropsSourceConnectionAdmin(options: ZeropsSourceConnectionAdminOptions): SourceConnectionAdmin {
97
- if (!PROJECT_ID_PATTERN.test(options.projectId)) throw new SourceConnectionAdminError('invalid_configuration')
98
- const attempts = options.reread?.attempts ?? DEFAULT_REREAD_ATTEMPTS
99
- const delayMs = options.reread?.delayMs ?? DEFAULT_REREAD_DELAY_MS
100
- if (!Number.isSafeInteger(attempts) || attempts <= 0 || attempts > 20 || !Number.isSafeInteger(delayMs) || delayMs < 0 || delayMs > 5_000) {
101
- throw new SourceConnectionAdminError('invalid_configuration')
102
- }
103
- const sleep = options.reread?.sleep ?? defaultSleep
104
-
105
- const readEnvironment = async (signal: AbortSignal): Promise<{ service: ZeropsService; environment: ZeropsServiceEnv[] }> => {
106
- throwIfAborted(signal)
107
- const service = await exactSourceService(options.api, options.projectId, signal)
108
- let environment: ZeropsServiceEnv[]
109
- try {
110
- environment = await options.api.listServiceEnv({ serviceId: service.id, signal })
111
- } catch (error) {
112
- if (isAbort(error, signal)) throw error
113
- throw new SourceConnectionAdminError('credential_persistence')
114
- }
115
- throwIfAborted(signal)
116
- return { service, environment }
117
- }
118
-
119
- const inspectV2Internal = async (connectionId: string, signal: AbortSignal): Promise<DurableInspectionV2> => {
120
- let environmentKey: string
121
- try {
122
- environmentKey = await zeropsSourceCredentialEnvV2(connectionId)
123
- } catch {
124
- throw new SourceConnectionAdminError('credential_conflict')
125
- }
126
- const current = await readEnvironment(signal)
127
- return await classifyV2(current.service, current.environment, environmentKey, connectionId)
128
- }
129
-
130
- const admin: SourceConnectionAdmin = {
131
- // Proves the exact `source` service AND that this token can read its environment. A setup that
132
- // cannot do both fails later anyway; the console shows this probe before it starts one.
133
- inspect: async (signal) => {
134
- await readEnvironment(signal)
135
- return { state: 'anonymous' }
136
- },
137
-
138
- activateV2: async (input) => {
139
- let digest: string
140
- try {
141
- const bundle = decodeZeropsSourceCredentialBundleV2(input.credentialBundle)
142
- if (bundle.connectionId !== input.connectionId) throw new Error('connection mismatch')
143
- digest = await sha256ZeropsSourceCredentialBundleV2(input.credentialBundle)
144
- } catch {
145
- throw new SourceConnectionAdminError('credential_conflict')
146
- }
147
- if (digest !== input.credentialSha256) throw new SourceConnectionAdminError('credential_conflict')
148
- const initial = await inspectV2Internal(input.connectionId, input.signal)
149
- if (initial.public.state === 'durable') {
150
- if (initial.bundle !== input.credentialBundle || initial.public.credentialSha256 !== digest) {
151
- throw new SourceConnectionAdminError('credential_conflict')
152
- }
153
- } else {
154
- try {
155
- await options.api.createServiceEnv({
156
- serviceId: initial.service.id,
157
- key: initial.environmentKey,
158
- value: input.credentialBundle,
159
- signal: input.signal,
160
- })
161
- } catch (error) {
162
- if (isAbort(error, input.signal)) throw error
163
- }
164
- await proveDurableV2(input, digest, attempts, delayMs, sleep, inspectV2Internal)
165
- }
166
- let activated: ZeropsSourceCredentialActivateResponseV2
167
- try {
168
- activated = await options.source.activateV2(input)
169
- } catch (error) {
170
- if (isAbort(error, input.signal)) throw error
171
- let status: ZeropsSourceCredentialStatusResponseV2
172
- try {
173
- status = await options.source.statusV2({ connectionId: input.connectionId, signal: input.signal })
174
- } catch (statusError) {
175
- if (isAbort(statusError, input.signal)) throw statusError
176
- throw new SourceConnectionAdminError('credential_activation')
177
- }
178
- if (
179
- status.state !== 'active' || status.connectionId !== input.connectionId || status.credentialSha256 !== digest
180
- ) throw new SourceConnectionAdminError('credential_activation')
181
- activated = {
182
- protocolVersion: ZEROPS_SOURCE_PROTOCOL_VERSION_V2,
183
- connectionId: input.connectionId,
184
- credentialVersion: ZEROPS_SOURCE_CREDENTIAL_BUNDLE_VERSION_V2,
185
- credentialSha256: status.credentialSha256,
186
- githubApp: status.githubApp,
187
- }
188
- }
189
- if (activated.connectionId !== input.connectionId || activated.credentialSha256 !== digest) {
190
- throw new SourceConnectionAdminError('credential_activation')
191
- }
192
- return activated
193
- },
194
-
195
- statusV2: async (input) => {
196
- const durable = await inspectV2Internal(input.connectionId, input.signal)
197
- if (durable.public.state !== 'durable') return durable.public
198
- let runtime: ZeropsSourceCredentialStatusResponseV2
199
- try {
200
- runtime = await options.source.statusV2(input)
201
- } catch (error) {
202
- if (isAbort(error, input.signal)) throw error
203
- throw new SourceConnectionAdminError('credential_activation')
204
- }
205
- if (runtime.connectionId !== input.connectionId || runtime.state === 'anonymous') {
206
- return { state: 'activation-required', credentialSha256: durable.public.credentialSha256 }
207
- }
208
- if (runtime.credentialSha256 !== durable.public.credentialSha256) {
209
- throw new SourceConnectionAdminError('credential_conflict')
210
- }
211
- return { state: 'active', credentialSha256: runtime.credentialSha256, githubApp: runtime.githubApp }
212
- },
213
-
214
- configureWebhook: async (input) => {
215
- await requireDurableDigest(input.connectionId, input.credentialSha256, input.signal, inspectV2Internal)
216
- try {
217
- const response = await options.source.configureWebhook(input)
218
- if (
219
- response.connectionId !== input.connectionId || response.credentialSha256 !== input.credentialSha256
220
- || response.webhook.url !== input.url || response.webhook.contentType !== 'json' || response.webhook.insecureSsl !== '0'
221
- ) throw new SourceConnectionAdminError('credential_activation')
222
- return response
223
- } catch (error) {
224
- if (isAbort(error, input.signal)) throw error
225
- if (error instanceof SourceConnectionAdminError) throw error
226
- throw new SourceConnectionAdminError('credential_activation')
227
- }
228
- },
229
-
230
- verifyInstallations: async (input) => {
231
- await requireDurableDigest(input.connectionId, input.credentialSha256, input.signal, inspectV2Internal)
232
- try {
233
- const response = await options.source.verifyInstallations(input)
234
- if (response.connectionId !== input.connectionId || response.credentialSha256 !== input.credentialSha256) {
235
- throw new SourceConnectionAdminError('credential_activation')
236
- }
237
- return response
238
- } catch (error) {
239
- if (isAbort(error, input.signal)) throw error
240
- if (error instanceof SourceConnectionAdminError) throw error
241
- throw new SourceConnectionAdminError('credential_activation')
242
- }
243
- },
244
- }
245
- return admin
246
- }
247
-
248
- async function requireDurableDigest(
249
- connectionId: string,
250
- digest: string,
251
- signal: AbortSignal,
252
- inspectV2: (connectionId: string, signal: AbortSignal) => Promise<DurableInspectionV2>,
253
- ): Promise<void> {
254
- const keyed = await inspectV2(connectionId, signal)
255
- if (keyed.public.state !== 'durable' || keyed.public.credentialSha256 !== digest) {
256
- throw new SourceConnectionAdminError('credential_conflict')
257
- }
258
- }
259
-
260
- async function exactSourceService(api: SourceConnectionZeropsApi, projectId: string, signal: AbortSignal): Promise<ZeropsService> {
261
- let service: ZeropsService | null
262
- try {
263
- service = await api.findService({ projectId, hostname: SOURCE_HOSTNAME, signal })
264
- } catch (error) {
265
- if (isAbort(error, signal)) throw error
266
- throw new SourceConnectionAdminError('source_not_found')
267
- }
268
- if (service === null) throw new SourceConnectionAdminError('source_not_found')
269
- if (service.id === '' || service.name !== SOURCE_HOSTNAME || service.projectId !== projectId) {
270
- throw new SourceConnectionAdminError('source_mismatch')
271
- }
272
- return service
273
- }
274
-
275
- async function classifyV2(
276
- service: ZeropsService,
277
- environment: readonly ZeropsServiceEnv[],
278
- environmentKey: string,
279
- connectionId: string,
280
- ): Promise<DurableInspectionV2> {
281
- const entries = environment.filter((entry) => entry.key === environmentKey)
282
- if (entries.length > 1) throw new SourceConnectionAdminError('credential_conflict')
283
- if (entries.length === 0) return { public: { state: 'anonymous' }, service, environmentKey }
284
- const bundle = entries[0]?.content
285
- if (bundle === undefined) throw new SourceConnectionAdminError('credential_conflict')
286
- try {
287
- const decoded = decodeZeropsSourceCredentialBundleV2(bundle)
288
- if (decoded.connectionId !== connectionId) throw new Error('connection mismatch')
289
- return {
290
- public: { state: 'durable', credentialSha256: await sha256ZeropsSourceCredentialBundleV2(bundle) },
291
- service,
292
- environmentKey,
293
- bundle,
294
- }
295
- } catch {
296
- throw new SourceConnectionAdminError('credential_conflict')
297
- }
298
- }
299
-
300
- async function proveDurableV2(
301
- input: SourceConnectionActivateInput,
302
- digest: string,
303
- attempts: number,
304
- delayMs: number,
305
- sleep: (delayMs: number, signal: AbortSignal) => Promise<void>,
306
- inspect: (connectionId: string, signal: AbortSignal) => Promise<DurableInspectionV2>,
307
- ): Promise<void> {
308
- for (let attempt = 0; attempt < attempts; attempt++) {
309
- const durable = await inspect(input.connectionId, input.signal)
310
- if (durable.public.state === 'durable') {
311
- if (durable.bundle === input.credentialBundle && durable.public.credentialSha256 === digest) return
312
- throw new SourceConnectionAdminError('credential_conflict')
313
- }
314
- if (attempt + 1 < attempts) await sleep(delayMs, input.signal)
315
- }
316
- throw new SourceConnectionAdminError('credential_persistence')
317
- }
318
-
319
- const defaultSleep = (delayMs: number, signal: AbortSignal): Promise<void> => {
320
- throwIfAborted(signal)
321
- return new Promise((resolve, reject) => {
322
- const timer = setTimeout(() => {
323
- cleanup()
324
- resolve()
325
- }, delayMs)
326
- const abort = (): void => {
327
- cleanup()
328
- reject(abortError())
329
- }
330
- const cleanup = (): void => {
331
- clearTimeout(timer)
332
- signal.removeEventListener('abort', abort)
333
- }
334
- signal.addEventListener('abort', abort, { once: true })
335
- })
336
- }
337
-
338
- const throwIfAborted = (signal: AbortSignal): void => {
339
- if (signal.aborted) throw abortError()
340
- }
341
-
342
- const isAbort = (error: unknown, signal: AbortSignal): boolean => signal.aborted || (error instanceof Error && error.name === 'AbortError')
343
-
344
- const abortError = (): DOMException => new DOMException('Source connection administration was aborted', 'AbortError')