@fabrika/provider-zerops 0.0.4 → 0.0.6
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.
- package/package.json +5 -5
- package/src/api.ts +137 -12
- package/src/cli.ts +42 -2
- package/src/codec.ts +70 -12
- package/src/collaborators.ts +5 -0
- package/src/control.ts +302 -43
- package/src/index.ts +119 -1
- package/src/manifest.ts +60 -3
- package/src/provider.ts +171 -31
- package/src/source-connection.ts +394 -0
- package/src/source.ts +1043 -0
- package/src/types.ts +18 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fabrika/provider-zerops",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -32,9 +32,9 @@
|
|
|
32
32
|
"gen:schema": "bun scripts/generate-zerops-types.ts"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@fabrika/auth": "^0.0.
|
|
36
|
-
"@fabrika/auth-core": "^0.0.
|
|
37
|
-
"@fabrika/provider-contract": "^0.0.
|
|
38
|
-
"@fabrika/proxy-contract": "^0.0.
|
|
35
|
+
"@fabrika/auth": "^0.0.6",
|
|
36
|
+
"@fabrika/auth-core": "^0.0.6",
|
|
37
|
+
"@fabrika/provider-contract": "^0.0.6",
|
|
38
|
+
"@fabrika/proxy-contract": "^0.0.6"
|
|
39
39
|
}
|
|
40
40
|
}
|
package/src/api.ts
CHANGED
|
@@ -62,6 +62,13 @@ export interface ZeropsAppVersion {
|
|
|
62
62
|
build?: { startDate?: string; endDate?: string; pipelineStart?: string; pipelineFinish?: string; pipelineFailed?: string }
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
/** The credential-bearing result of reserving one upload-backed application version. */
|
|
66
|
+
export interface ZeropsAppVersionUpload {
|
|
67
|
+
id: string
|
|
68
|
+
/** Presigned object-storage destination. Never log it or include it in an error. */
|
|
69
|
+
uploadUrl: string
|
|
70
|
+
}
|
|
71
|
+
|
|
65
72
|
/** An asynchronous platform operation. VERIFIED: `OutDtoProcess`. */
|
|
66
73
|
export interface ZeropsProcess {
|
|
67
74
|
id: string
|
|
@@ -268,10 +275,10 @@ export interface ZeropsLogLine {
|
|
|
268
275
|
* case cancellation was added for, and an abandoned run must not keep an HTTP request alive.
|
|
269
276
|
*
|
|
270
277
|
* WIDER THAN THE DRIVER, on purpose. The driver uses four of these; the rest are the CONTROL PLANE's
|
|
271
|
-
* Zerops surface —
|
|
272
|
-
*
|
|
273
|
-
*
|
|
274
|
-
*
|
|
278
|
+
* Zerops surface — the service-env methods for the edit-time secret writes that ADR-0004 says are not a
|
|
279
|
+
* deploy step, `importProject` for creating a managed project, and `getAppVersion`/`latestAppVersion` for
|
|
280
|
+
* the startup reconciliation of in-flight runs that ADR-0003 makes a requirement. One client, so there is
|
|
281
|
+
* one place the Zerops contract is written down.
|
|
275
282
|
*
|
|
276
283
|
* NOTE WHAT IS ABSENT. There is no method that writes a PROJECT-level environment variable. That is not
|
|
277
284
|
* an oversight and must not be "completed": project-level variables are injected into every service in the
|
|
@@ -329,6 +336,27 @@ export interface ZeropsApi {
|
|
|
329
336
|
signal: AbortSignal
|
|
330
337
|
}): Promise<ZeropsProcess | null>
|
|
331
338
|
|
|
339
|
+
/**
|
|
340
|
+
* Reserve an upload-backed application version. VERIFIED: `POST /service-stack/{id}/app-version`,
|
|
341
|
+
* optional body `{ name }`, response `ResponsePostAppVersion`.
|
|
342
|
+
*/
|
|
343
|
+
createAppVersion(input: { serviceId: string; name?: string; signal: AbortSignal }): Promise<ZeropsAppVersionUpload>
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Build and deploy an application version whose source archive has already been uploaded. VERIFIED:
|
|
347
|
+
* `PUT /app-version/{id}/build-and-deploy`, required body `{ zeropsYaml }` and optional
|
|
348
|
+
* `zeropsYamlSetup`.
|
|
349
|
+
*/
|
|
350
|
+
buildAndDeployAppVersion(input: {
|
|
351
|
+
appVersionId: string
|
|
352
|
+
zeropsYaml: string
|
|
353
|
+
zeropsYamlSetup?: string
|
|
354
|
+
signal: AbortSignal
|
|
355
|
+
}): Promise<ZeropsProcess>
|
|
356
|
+
|
|
357
|
+
/** Delete an application version, including an untriggered upload reservation. VERIFIED: `DELETE /app-version/{id}`. */
|
|
358
|
+
deleteAppVersion(input: { appVersionId: string; signal: AbortSignal }): Promise<void>
|
|
359
|
+
|
|
332
360
|
/** One application version's current state — the poll target. VERIFIED: `GET /app-version/{id}`. */
|
|
333
361
|
getAppVersion(input: { appVersionId: string; signal: AbortSignal }): Promise<ZeropsAppVersion>
|
|
334
362
|
|
|
@@ -420,6 +448,16 @@ export interface ZeropsApi {
|
|
|
420
448
|
*/
|
|
421
449
|
listServiceEnv(input: { serviceId: string; signal: AbortSignal }): Promise<ZeropsServiceEnv[]>
|
|
422
450
|
|
|
451
|
+
/**
|
|
452
|
+
* Create ONE service-level variable only when the key is absent.
|
|
453
|
+
* VERIFIED: `POST /service-stack/{id}/user-data`, body `{ key, content }`.
|
|
454
|
+
*
|
|
455
|
+
* Unlike `putServiceEnv`, a duplicate is an error and is never followed by a read or update. Error
|
|
456
|
+
* details are intentionally unavailable because both the request and an upstream response can contain
|
|
457
|
+
* the secret value.
|
|
458
|
+
*/
|
|
459
|
+
createServiceEnv(input: { serviceId: string; key: string; value: string; signal: AbortSignal }): Promise<void>
|
|
460
|
+
|
|
423
461
|
/**
|
|
424
462
|
* Create-or-update ONE service-level variable — the only way fabrika ever writes a secret, and it is
|
|
425
463
|
* addressed BY SERVICE so ADR-0004's invariant cannot be violated by calling it.
|
|
@@ -745,6 +783,8 @@ const apiError = (label: string, status: number, body: unknown, redactDetail: bo
|
|
|
745
783
|
export const createZeropsApi = (options: ZeropsApiOptions): ZeropsApi => {
|
|
746
784
|
const base = (options.baseUrl ?? ZEROPS_API_BASE).replace(/\/+$/, '')
|
|
747
785
|
const doFetch = options.fetchImpl ?? fetch
|
|
786
|
+
const abortError = (): DOMException => new DOMException('The operation was aborted', 'AbortError')
|
|
787
|
+
const isAbortError = (error: unknown): boolean => error instanceof Error && error.name === 'AbortError'
|
|
748
788
|
|
|
749
789
|
const request = async (
|
|
750
790
|
label: string,
|
|
@@ -754,26 +794,56 @@ export const createZeropsApi = (options: ZeropsApiOptions): ZeropsApi => {
|
|
|
754
794
|
* `redactDetail` drops the server's message from the thrown error, keeping only its code. Set it on
|
|
755
795
|
* any call whose BODY carries a secret value: a validation error can quote what it rejected, and an
|
|
756
796
|
* error that echoes a secret into a deploy log is a leak whichever end wrote it.
|
|
797
|
+
* `omitErrorResponseDetails` also drops the code for upload flows whose response may carry a
|
|
798
|
+
* credential; nothing from that response body may reach an error.
|
|
757
799
|
*/
|
|
758
|
-
init: { body?: unknown; signal: AbortSignal; redactDetail?: boolean },
|
|
800
|
+
init: { body?: unknown; signal: AbortSignal; redactDetail?: boolean; omitErrorResponseDetails?: boolean },
|
|
759
801
|
): Promise<unknown> => {
|
|
802
|
+
if (init.signal.aborted) {
|
|
803
|
+
throw abortError()
|
|
804
|
+
}
|
|
760
805
|
const headers: Record<string, string> = { authorization: `Bearer ${options.token}`, accept: 'application/json' }
|
|
761
806
|
if (init.body !== undefined) {
|
|
762
807
|
headers['content-type'] = 'application/json'
|
|
763
808
|
}
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
809
|
+
let response: Response
|
|
810
|
+
try {
|
|
811
|
+
response = await doFetch(`${base}${path}`, {
|
|
812
|
+
method,
|
|
813
|
+
headers,
|
|
814
|
+
body: init.body === undefined ? undefined : JSON.stringify(init.body),
|
|
815
|
+
signal: init.signal,
|
|
816
|
+
})
|
|
817
|
+
} catch (error) {
|
|
818
|
+
if (init.signal.aborted || isAbortError(error)) throw abortError()
|
|
819
|
+
throw error
|
|
820
|
+
}
|
|
821
|
+
let payload: unknown
|
|
822
|
+
try {
|
|
823
|
+
payload = await response.json()
|
|
824
|
+
} catch (error) {
|
|
825
|
+
if (init.signal.aborted || isAbortError(error)) throw abortError()
|
|
826
|
+
payload = null
|
|
827
|
+
}
|
|
771
828
|
if (!response.ok) {
|
|
829
|
+
if (init.omitErrorResponseDetails === true) {
|
|
830
|
+
throw new ZeropsApiError(`zerops: ${label} failed (${response.status})`, response.status, '')
|
|
831
|
+
}
|
|
772
832
|
throw apiError(label, response.status, payload, init.redactDetail === true)
|
|
773
833
|
}
|
|
774
834
|
return payload
|
|
775
835
|
}
|
|
776
836
|
|
|
837
|
+
const invalidResponse = (label: string): Error => new Error(`zerops: ${label} returned an invalid response`)
|
|
838
|
+
|
|
839
|
+
const requiredString = (payload: unknown, key: string, label: string): string => {
|
|
840
|
+
const value = str(payload, key)
|
|
841
|
+
if (value === undefined || value.trim() === '') {
|
|
842
|
+
throw invalidResponse(label)
|
|
843
|
+
}
|
|
844
|
+
return value
|
|
845
|
+
}
|
|
846
|
+
|
|
777
847
|
const readImportResult = (payload: unknown): ZeropsImportResult => ({
|
|
778
848
|
projectId: str(payload, 'projectId') ?? '',
|
|
779
849
|
projectName: str(payload, 'projectName'),
|
|
@@ -844,6 +914,48 @@ export const createZeropsApi = (options: ZeropsApiOptions): ZeropsApi => {
|
|
|
844
914
|
return process === undefined || process === null ? null : readProcess(process)
|
|
845
915
|
},
|
|
846
916
|
|
|
917
|
+
createAppVersion: async ({ serviceId, name, signal }) => {
|
|
918
|
+
const body = name === undefined ? {} : { name }
|
|
919
|
+
const payload = await request('create app-version', 'POST', `/service-stack/${serviceId}/app-version`, {
|
|
920
|
+
body,
|
|
921
|
+
signal,
|
|
922
|
+
omitErrorResponseDetails: true,
|
|
923
|
+
})
|
|
924
|
+
const responseServiceId = requiredString(payload, 'serviceStackId', 'create app-version')
|
|
925
|
+
if (responseServiceId !== serviceId) {
|
|
926
|
+
throw invalidResponse('create app-version')
|
|
927
|
+
}
|
|
928
|
+
return {
|
|
929
|
+
id: requiredString(payload, 'id', 'create app-version'),
|
|
930
|
+
uploadUrl: requiredString(payload, 'uploadUrl', 'create app-version'),
|
|
931
|
+
}
|
|
932
|
+
},
|
|
933
|
+
|
|
934
|
+
buildAndDeployAppVersion: async ({ appVersionId, zeropsYaml, zeropsYamlSetup, signal }) => {
|
|
935
|
+
const body = zeropsYamlSetup === undefined ? { zeropsYaml } : { zeropsYaml, zeropsYamlSetup }
|
|
936
|
+
const payload = await request('build and deploy app-version', 'PUT', `/app-version/${appVersionId}/build-and-deploy`, {
|
|
937
|
+
body,
|
|
938
|
+
signal,
|
|
939
|
+
omitErrorResponseDetails: true,
|
|
940
|
+
})
|
|
941
|
+
const processId = requiredString(payload, 'id', 'build and deploy app-version')
|
|
942
|
+
const responseAppVersionId = str(prop(payload, 'appVersion'), 'id')
|
|
943
|
+
if (responseAppVersionId !== undefined && (responseAppVersionId.trim() === '' || responseAppVersionId !== appVersionId)) {
|
|
944
|
+
throw invalidResponse('build and deploy app-version')
|
|
945
|
+
}
|
|
946
|
+
return { ...readProcess(payload), id: processId, appVersionId }
|
|
947
|
+
},
|
|
948
|
+
|
|
949
|
+
deleteAppVersion: async ({ appVersionId, signal }) => {
|
|
950
|
+
const payload = await request('delete app-version', 'DELETE', `/app-version/${appVersionId}`, {
|
|
951
|
+
signal,
|
|
952
|
+
omitErrorResponseDetails: true,
|
|
953
|
+
})
|
|
954
|
+
if (prop(payload, 'success') !== true) {
|
|
955
|
+
throw invalidResponse('delete app-version')
|
|
956
|
+
}
|
|
957
|
+
},
|
|
958
|
+
|
|
847
959
|
getAppVersion: async ({ appVersionId, signal }) =>
|
|
848
960
|
readAppVersion(await request('get app-version', 'GET', `/app-version/${appVersionId}`, { signal })),
|
|
849
961
|
|
|
@@ -904,6 +1016,19 @@ export const createZeropsApi = (options: ZeropsApiOptions): ZeropsApi => {
|
|
|
904
1016
|
|
|
905
1017
|
listServiceEnv,
|
|
906
1018
|
|
|
1019
|
+
createServiceEnv: async ({ serviceId, key, value, signal }) => {
|
|
1020
|
+
try {
|
|
1021
|
+
await request('create-only service env', 'POST', `/service-stack/${serviceId}/user-data`, {
|
|
1022
|
+
body: { key, content: value },
|
|
1023
|
+
signal,
|
|
1024
|
+
omitErrorResponseDetails: true,
|
|
1025
|
+
})
|
|
1026
|
+
} catch (error) {
|
|
1027
|
+
if (error instanceof Error && error.name === 'AbortError') throw error
|
|
1028
|
+
throw new Error('zerops: create-only service env failed')
|
|
1029
|
+
}
|
|
1030
|
+
},
|
|
1031
|
+
|
|
907
1032
|
putServiceEnv: async ({ serviceId, key, value, signal }) => {
|
|
908
1033
|
// `redactDetail` on both writes: this is the one place a secret VALUE is in the request body.
|
|
909
1034
|
try {
|
package/src/cli.ts
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
+
import { constants } from 'node:fs'
|
|
3
|
+
import { open } from 'node:fs/promises'
|
|
2
4
|
import { resolve } from 'node:path'
|
|
3
5
|
import { isZeropsAppConfig } from './authoring'
|
|
4
6
|
import { parseZeropsCliArgs } from './cli-args'
|
|
5
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
compileFabrikaManifest,
|
|
9
|
+
createZeropsArtifactSourceDescriptor,
|
|
10
|
+
manifestServiceHostnames,
|
|
11
|
+
ZEROPS_SOURCE_DESCRIPTOR_MAX_BYTES,
|
|
12
|
+
} from './manifest'
|
|
6
13
|
import { runZeropsNamespaceCommand } from './namespace-command'
|
|
7
14
|
import type { ZeropsAppConfig } from './types'
|
|
8
15
|
|
|
@@ -47,6 +54,38 @@ const loadConfig = async (path: string): Promise<ZeropsAppConfig> => {
|
|
|
47
54
|
return config
|
|
48
55
|
}
|
|
49
56
|
|
|
57
|
+
export const readZeropsSourceDescriptor = async (root: string = process.cwd()) => {
|
|
58
|
+
const path = resolve(root, 'zerops.yaml')
|
|
59
|
+
const file = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK).catch((error: unknown) => {
|
|
60
|
+
if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
|
|
61
|
+
throw new Error(`Missing repository-root Zerops source descriptor: ${path}`)
|
|
62
|
+
}
|
|
63
|
+
if (error instanceof Error && 'code' in error && error.code === 'ELOOP') {
|
|
64
|
+
throw new Error(`Repository-root Zerops source descriptor must not be a symlink: ${path}`)
|
|
65
|
+
}
|
|
66
|
+
throw error
|
|
67
|
+
})
|
|
68
|
+
try {
|
|
69
|
+
const metadata = await file.stat()
|
|
70
|
+
if (!metadata.isFile()) {
|
|
71
|
+
throw new Error(`Repository-root Zerops source descriptor must be a regular file: ${path}`)
|
|
72
|
+
}
|
|
73
|
+
if (metadata.size > ZEROPS_SOURCE_DESCRIPTOR_MAX_BYTES) {
|
|
74
|
+
throw new Error(`Zerops source descriptor exceeds ${ZEROPS_SOURCE_DESCRIPTOR_MAX_BYTES} bytes: ${path}`)
|
|
75
|
+
}
|
|
76
|
+
const bytes = await file.readFile()
|
|
77
|
+
let contents: string
|
|
78
|
+
try {
|
|
79
|
+
contents = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bytes)
|
|
80
|
+
} catch {
|
|
81
|
+
throw new Error(`Zerops source descriptor is not valid UTF-8: ${path}`)
|
|
82
|
+
}
|
|
83
|
+
return await createZeropsArtifactSourceDescriptor(contents)
|
|
84
|
+
} finally {
|
|
85
|
+
await file.close()
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
50
89
|
export const runZeropsCli = async (argv: readonly string[]): Promise<void> => {
|
|
51
90
|
const args = parseZeropsCliArgs(argv)
|
|
52
91
|
if (args.help || args.command === undefined) {
|
|
@@ -68,7 +107,8 @@ export const runZeropsCli = async (argv: readonly string[]): Promise<void> => {
|
|
|
68
107
|
throw new Error('`build` requires --env=<env>')
|
|
69
108
|
}
|
|
70
109
|
const config = await loadConfig(args.config)
|
|
71
|
-
const
|
|
110
|
+
const sourceDescriptor = await readZeropsSourceDescriptor()
|
|
111
|
+
const manifest = compileFabrikaManifest(config, args.env, sourceDescriptor)
|
|
72
112
|
const output = resolve(process.cwd(), args.output)
|
|
73
113
|
await Bun.write(output, `${JSON.stringify(manifest, null, '\t')}\n`)
|
|
74
114
|
console.info(`wrote ${output} (${manifestServiceHostnames(manifest).length} service(s))`)
|
package/src/codec.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { JsonValue, ProviderCodec } from '@fabrika/provider-contract'
|
|
2
|
-
import type { ZeropsRuntimeTarget } from './types'
|
|
2
|
+
import type { ZeropsRuntimeSource, ZeropsRuntimeTarget } from './types'
|
|
3
3
|
|
|
4
4
|
const field = (payload: JsonValue, key: string): JsonValue | undefined => {
|
|
5
5
|
if (typeof payload !== 'object' || payload === null || Array.isArray(payload)) {
|
|
@@ -27,24 +27,82 @@ const optionalString = (payload: JsonValue, key: string): string | undefined =>
|
|
|
27
27
|
return value
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
const IMMUTABLE_GIT_OBJECT = /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/
|
|
31
|
+
|
|
32
|
+
const object = (value: JsonValue | undefined, label: string): { readonly [key: string]: JsonValue } => {
|
|
33
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
34
|
+
throw new Error(`${label} must be an object`)
|
|
35
|
+
}
|
|
36
|
+
return value
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const runtimeSource = (payload: JsonValue): ZeropsRuntimeSource | undefined => {
|
|
40
|
+
const value = field(payload, 'source')
|
|
41
|
+
if (value === undefined) return undefined
|
|
42
|
+
const source = object(value, 'Zerops target source')
|
|
43
|
+
const repository = object(source['repository'], 'Zerops target source repository')
|
|
44
|
+
const allowedSourceKeys = ['runId', 'repository', 'commitSha', 'githubInstallationId']
|
|
45
|
+
const allowedRepositoryKeys = ['owner', 'name']
|
|
46
|
+
if (Object.keys(source).some((key) => !allowedSourceKeys.includes(key))) {
|
|
47
|
+
throw new Error('Zerops target source contains an unknown field')
|
|
48
|
+
}
|
|
49
|
+
if (Object.keys(repository).some((key) => !allowedRepositoryKeys.includes(key))) {
|
|
50
|
+
throw new Error('Zerops target source repository contains an unknown field')
|
|
51
|
+
}
|
|
52
|
+
const installation = source['githubInstallationId']
|
|
53
|
+
if (installation !== undefined && (typeof installation !== 'number' || !Number.isSafeInteger(installation) || installation <= 0)) {
|
|
54
|
+
throw new Error('Zerops target source githubInstallationId must be a positive safe integer')
|
|
55
|
+
}
|
|
56
|
+
const commitSha = requiredString(source, 'commitSha')
|
|
57
|
+
if (!IMMUTABLE_GIT_OBJECT.test(commitSha)) {
|
|
58
|
+
throw new Error('Zerops target source commitSha must be an exact lowercase Git object id')
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
runId: requiredString(source, 'runId'),
|
|
62
|
+
repository: {
|
|
63
|
+
owner: requiredString(repository, 'owner'),
|
|
64
|
+
name: requiredString(repository, 'name'),
|
|
65
|
+
},
|
|
66
|
+
commitSha,
|
|
67
|
+
...(installation === undefined ? {} : { githubInstallationId: installation }),
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
30
71
|
export const zeropsTargetCodec: ProviderCodec<ZeropsRuntimeTarget> = {
|
|
31
|
-
version:
|
|
72
|
+
version: 2,
|
|
32
73
|
encode: (target) => ({
|
|
33
74
|
projectId: target.projectId,
|
|
34
75
|
serviceId: target.serviceId,
|
|
35
76
|
accessToken: target.accessToken,
|
|
36
|
-
...(target.
|
|
77
|
+
...(target.source === undefined
|
|
78
|
+
? {}
|
|
79
|
+
: {
|
|
80
|
+
source: {
|
|
81
|
+
runId: target.source.runId,
|
|
82
|
+
repository: { owner: target.source.repository.owner, name: target.source.repository.name },
|
|
83
|
+
commitSha: target.source.commitSha,
|
|
84
|
+
...(target.source.githubInstallationId === undefined ? {} : { githubInstallationId: target.source.githubInstallationId }),
|
|
85
|
+
},
|
|
86
|
+
}),
|
|
37
87
|
...(target.apiBaseUrl !== undefined ? { apiBaseUrl: target.apiBaseUrl } : {}),
|
|
38
88
|
...(target.propustkaUrl !== undefined ? { propustkaUrl: target.propustkaUrl } : {}),
|
|
39
89
|
...(target.adminKey !== undefined ? { adminKey: target.adminKey } : {}),
|
|
40
90
|
}),
|
|
41
|
-
decode: (payload) =>
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
91
|
+
decode: (payload) => {
|
|
92
|
+
const target = object(payload, 'Zerops target')
|
|
93
|
+
const allowedKeys = ['projectId', 'serviceId', 'accessToken', 'source', 'apiBaseUrl', 'propustkaUrl', 'adminKey']
|
|
94
|
+
if (Object.keys(target).some((key) => !allowedKeys.includes(key))) {
|
|
95
|
+
throw new Error('Zerops target contains an unknown field')
|
|
96
|
+
}
|
|
97
|
+
const source = runtimeSource(payload)
|
|
98
|
+
return {
|
|
99
|
+
projectId: requiredString(payload, 'projectId'),
|
|
100
|
+
serviceId: requiredString(payload, 'serviceId'),
|
|
101
|
+
accessToken: requiredString(payload, 'accessToken'),
|
|
102
|
+
...(source === undefined ? {} : { source }),
|
|
103
|
+
...(optionalString(payload, 'apiBaseUrl') !== undefined ? { apiBaseUrl: optionalString(payload, 'apiBaseUrl') } : {}),
|
|
104
|
+
...(optionalString(payload, 'propustkaUrl') !== undefined ? { propustkaUrl: optionalString(payload, 'propustkaUrl') } : {}),
|
|
105
|
+
...(optionalString(payload, 'adminKey') !== undefined ? { adminKey: optionalString(payload, 'adminKey') } : {}),
|
|
106
|
+
}
|
|
107
|
+
},
|
|
50
108
|
}
|
package/src/collaborators.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { reconcileSchema } from '@fabrika/auth'
|
|
2
2
|
import type { SchemaReconciler } from '@fabrika/provider-contract'
|
|
3
3
|
import { createZeropsApi, type ZeropsApi } from './api'
|
|
4
|
+
import type { ZeropsSourceClient } from './source'
|
|
4
5
|
import type { ZeropsRuntimeTarget } from './types'
|
|
5
6
|
|
|
6
7
|
export type Sleeper = (ms: number, signal: AbortSignal) => Promise<void>
|
|
@@ -24,6 +25,10 @@ export const defaultSleep: Sleeper = (ms, signal) =>
|
|
|
24
25
|
|
|
25
26
|
export interface ZeropsCollaborators {
|
|
26
27
|
api: ZeropsApi
|
|
28
|
+
/** Injected by the Zerops control composition; dry runs do not require it. */
|
|
29
|
+
source?: ZeropsSourceClient
|
|
30
|
+
/** Independently bounds best-effort source cancellation without delaying Zerops cleanup. */
|
|
31
|
+
sourceCancelSleep?: Sleeper
|
|
27
32
|
reconcileSchema: SchemaReconciler
|
|
28
33
|
sleep: Sleeper
|
|
29
34
|
}
|