@fabrika/provider-zerops 0.0.8 → 0.0.9

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fabrika/provider-zerops",
3
- "version": "0.0.8",
3
+ "version": "0.0.9",
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.8",
36
- "@fabrika/auth-core": "^0.0.8",
37
- "@fabrika/provider-contract": "^0.0.8",
38
- "@fabrika/proxy-contract": "^0.0.8"
35
+ "@fabrika/auth": "^0.0.9",
36
+ "@fabrika/auth-core": "^0.0.9",
37
+ "@fabrika/provider-contract": "^0.0.9",
38
+ "@fabrika/proxy-contract": "^0.0.9"
39
39
  }
40
40
  }
package/src/api.ts CHANGED
@@ -445,6 +445,8 @@ export interface ZeropsApi {
445
445
  * NOT `GET /service-stack/{id}/user-data`, which answers `400 serviceStackNotFound` on every
446
446
  * service, deployed or not — see `docs/reference/zerops-platform.md`. The `/env` reading omits
447
447
  * variables the service's own `zerops.yaml` declares (`type: ENV`); fabrika never writes those.
448
+ * The OpenAPI publishes no lookup/pagination terms and live probes found query terms ignored, so the
449
+ * implementation reads one full response under a byte bound rather than imposing an entry-count cap.
448
450
  */
449
451
  listServiceEnv(input: { serviceId: string; signal: AbortSignal }): Promise<ZeropsServiceEnv[]>
450
452
 
@@ -785,6 +787,58 @@ export const createZeropsApi = (options: ZeropsApiOptions): ZeropsApi => {
785
787
  const doFetch = options.fetchImpl ?? fetch
786
788
  const abortError = (): DOMException => new DOMException('The operation was aborted', 'AbortError')
787
789
  const isAbortError = (error: unknown): boolean => error instanceof Error && error.name === 'AbortError'
790
+ const SERVICE_ENV_RESPONSE_MAX_BYTES = 8 * 1024 * 1024
791
+ class ResponseByteBoundError extends Error {}
792
+
793
+ const boundedJson = async (response: Response, label: string, maxBytes: number, signal: AbortSignal): Promise<unknown> => {
794
+ if (signal.aborted) {
795
+ void response.body?.cancel().catch(() => {})
796
+ throw abortError()
797
+ }
798
+ const contentLength = response.headers.get('content-length')
799
+ if (contentLength !== null) {
800
+ const declaredBytes = Number(contentLength)
801
+ if (Number.isFinite(declaredBytes) && declaredBytes > maxBytes) {
802
+ void response.body?.cancel().catch(() => {})
803
+ throw new ResponseByteBoundError(`zerops: ${label} response exceeded its byte bound`)
804
+ }
805
+ }
806
+ if (response.body === null) return null
807
+ const reader = response.body.getReader()
808
+ let abortRead = (): void => {}
809
+ const aborted = new Promise<never>((_resolve, reject) => {
810
+ abortRead = (): void => {
811
+ void reader.cancel().catch(() => {})
812
+ reject(abortError())
813
+ }
814
+ signal.addEventListener('abort', abortRead, { once: true })
815
+ if (signal.aborted) abortRead()
816
+ })
817
+ const chunks: Uint8Array[] = []
818
+ let receivedBytes = 0
819
+ try {
820
+ while (true) {
821
+ const next = await Promise.race([reader.read(), aborted])
822
+ if (signal.aborted) throw abortError()
823
+ if (next.done) break
824
+ receivedBytes += next.value.byteLength
825
+ if (receivedBytes > maxBytes) {
826
+ void reader.cancel().catch(() => {})
827
+ throw new ResponseByteBoundError(`zerops: ${label} response exceeded its byte bound`)
828
+ }
829
+ chunks.push(next.value)
830
+ }
831
+ } finally {
832
+ signal.removeEventListener('abort', abortRead)
833
+ }
834
+ const bytes = new Uint8Array(receivedBytes)
835
+ let offset = 0
836
+ for (const chunk of chunks) {
837
+ bytes.set(chunk, offset)
838
+ offset += chunk.byteLength
839
+ }
840
+ return JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes))
841
+ }
788
842
 
789
843
  const request = async (
790
844
  label: string,
@@ -797,7 +851,13 @@ export const createZeropsApi = (options: ZeropsApiOptions): ZeropsApi => {
797
851
  * `omitErrorResponseDetails` also drops the code for upload flows whose response may carry a
798
852
  * credential; nothing from that response body may reach an error.
799
853
  */
800
- init: { body?: unknown; signal: AbortSignal; redactDetail?: boolean; omitErrorResponseDetails?: boolean },
854
+ init: {
855
+ body?: unknown
856
+ signal: AbortSignal
857
+ redactDetail?: boolean
858
+ omitErrorResponseDetails?: boolean
859
+ responseMaxBytes?: number
860
+ },
801
861
  ): Promise<unknown> => {
802
862
  if (init.signal.aborted) {
803
863
  throw abortError()
@@ -820,9 +880,12 @@ export const createZeropsApi = (options: ZeropsApiOptions): ZeropsApi => {
820
880
  }
821
881
  let payload: unknown
822
882
  try {
823
- payload = await response.json()
883
+ payload = init.responseMaxBytes === undefined
884
+ ? await response.json()
885
+ : await boundedJson(response, label, init.responseMaxBytes, init.signal)
824
886
  } catch (error) {
825
887
  if (init.signal.aborted || isAbortError(error)) throw abortError()
888
+ if (error instanceof ResponseByteBoundError) throw error
826
889
  payload = null
827
890
  }
828
891
  if (!response.ok) {
@@ -887,8 +950,13 @@ export const createZeropsApi = (options: ZeropsApiOptions): ZeropsApi => {
887
950
  * that hands it over on every service is `/env` (`/user-data` answers 400 unconditionally).
888
951
  */
889
952
  const listServiceEnv: ZeropsApi['listServiceEnv'] = async ({ serviceId, signal }) => {
890
- const payload = await request('list service env', 'GET', `/service-stack/${serviceId}/env`, { signal })
891
- return arr(payload, 'items').map(readServiceEnv)
953
+ const payload = await request('list service env', 'GET', `/service-stack/${serviceId}/env`, {
954
+ signal,
955
+ responseMaxBytes: SERVICE_ENV_RESPONSE_MAX_BYTES,
956
+ })
957
+ const items = prop(payload, 'items')
958
+ if (!Array.isArray(items)) throw invalidResponse('list service env')
959
+ return items.map(readServiceEnv)
892
960
  }
893
961
 
894
962
  return {
package/src/control.ts CHANGED
@@ -10,6 +10,8 @@ import type {
10
10
  ProviderReconcileOutcome,
11
11
  ProviderRegistration,
12
12
  ProviderRegistrationInput,
13
+ ProviderSourceResolution,
14
+ ProviderSourceResolutionInput,
13
15
  ProviderTerminalOutcome,
14
16
  RuntimeProvider,
15
17
  RuntimeProviderRun,
@@ -35,8 +37,8 @@ import {
35
37
  zeropsArtifactCodec,
36
38
  } from './manifest'
37
39
  import { createZeropsNamespaceCapabilities, type ZeropsNamespaceTarget, zeropsNamespaceTargetCodec } from './namespace'
38
- import { createZeropsProvider } from './provider'
39
- import { normalizeZeropsSourceRepository, type ZeropsSourceClient } from './source'
40
+ import { createZeropsProvider, type ZeropsSourceTransportBinding } from './provider'
41
+ import { normalizeZeropsSourceRepository, type ZeropsSourceClient, type ZeropsSourceClientV2, type ZeropsSourceResolveResult } from './source'
40
42
  import type { ZeropsRunState, ZeropsRuntimeTarget } from './types'
41
43
 
42
44
  /** Provider coordinates that are safe to persist. Credentials are composed only for a live run. */
@@ -109,7 +111,7 @@ export interface ZeropsControlProviderOptions {
109
111
  readonly propustkaUrl?: string
110
112
  readonly adminKey?: string
111
113
  readonly api?: ZeropsApi
112
- readonly source: ZeropsSourceClient
114
+ readonly source: ZeropsSourceClient & Partial<ZeropsSourceClientV2>
113
115
  readonly reconcileSchema?: SchemaReconciler
114
116
  readonly sleep?: Sleeper
115
117
  readonly sourceCancelSleep?: Sleeper
@@ -123,6 +125,19 @@ export interface ZeropsControlProviderOptions {
123
125
  }
124
126
  }
125
127
 
128
+ export interface ZeropsBoundSourceResolutionInput extends ProviderSourceResolutionInput {
129
+ readonly sourceBinding: ZeropsSourceTransportBinding
130
+ }
131
+
132
+ export interface ZeropsBoundDeployInput extends ProviderDeployInput {
133
+ readonly sourceBinding: ZeropsSourceTransportBinding
134
+ }
135
+
136
+ export interface ZeropsControlProvider extends ControlProvider {
137
+ resolveSourceWithBinding(input: ZeropsBoundSourceResolutionInput): Promise<ProviderSourceResolution>
138
+ deployWithBinding(input: ZeropsBoundDeployInput): Promise<ProviderTerminalOutcome>
139
+ }
140
+
126
141
  interface DecodedZeropsEnvironment {
127
142
  readonly target: ZeropsStoredTarget
128
143
  readonly namespace: ProviderDeploymentNamespace
@@ -184,6 +199,9 @@ const normalizeRegistration = (input: ProviderRegistrationInput): ProviderRegist
184
199
  if (input.app.id !== input.environment.appId) {
185
200
  throw new Error(`Zerops environment belongs to app \`${input.environment.appId}\`, expected \`${input.app.id}\``)
186
201
  }
202
+ if ((input.app.source.githubConnectionId === undefined) !== (input.app.source.githubInstallationId === undefined)) {
203
+ throw new Error('Zerops private source requires both connection and installation coordinates')
204
+ }
187
205
  const decoded = decodeEnvironment(input.environment)
188
206
  const artifact = parseFabrikaManifest(
189
207
  decodeEnvelope('artifact', input.environment.artifact, zeropsArtifactCodec),
@@ -252,11 +270,12 @@ const buildTriggeredState = (
252
270
  })
253
271
 
254
272
  /** Build the complete Zerops lifecycle bundle without importing control-plane persistence. */
255
- export const createZeropsControlProvider = (options: ZeropsControlProviderOptions): ControlProvider => {
273
+ export const createZeropsControlProvider = (options: ZeropsControlProviderOptions): ZeropsControlProvider => {
256
274
  if (options.accessToken === '') {
257
275
  throw new Error('Zerops access token must be a non-empty string')
258
276
  }
259
277
  const api = options.api ?? createZeropsApi({ token: options.accessToken, baseUrl: options.apiBaseUrl })
278
+ const sourceBindings = new Map<string, ZeropsSourceTransportBinding>()
260
279
  const runtimeProvider = createZeropsProvider((target) => {
261
280
  const defaults = defaultZeropsCollaborators(target)
262
281
  return {
@@ -266,6 +285,13 @@ export const createZeropsControlProvider = (options: ZeropsControlProviderOption
266
285
  reconcileSchema: options.reconcileSchema ?? defaults.reconcileSchema,
267
286
  sleep: options.sleep ?? defaultSleep,
268
287
  }
288
+ }, {
289
+ bindingForRun: (runId) => sourceBindings.get(runId),
290
+ uploadV2: async (input) => {
291
+ const uploadV2 = options.source.uploadV2
292
+ if (uploadV2 === undefined) throw new Error('Zerops keyed source transport is unavailable')
293
+ return uploadV2.call(options.source, input)
294
+ },
269
295
  })
270
296
  const namespaceCapabilities = options.namespaces === undefined
271
297
  ? undefined
@@ -349,35 +375,68 @@ export const createZeropsControlProvider = (options: ZeropsControlProviderOption
349
375
  return { state: 'running' }
350
376
  }
351
377
 
352
- return {
378
+ const assertSourceBinding = (
379
+ input: Pick<ProviderDeployInput, 'app'>,
380
+ binding: ZeropsSourceTransportBinding | undefined,
381
+ ): void => {
382
+ const connectionId = input.app.source.githubConnectionId
383
+ const installationId = input.app.source.githubInstallationId
384
+ if (binding === undefined) {
385
+ if (connectionId !== undefined || installationId !== undefined) {
386
+ throw new Error('Zerops private source requires an explicit transport binding')
387
+ }
388
+ return
389
+ }
390
+ if (connectionId !== binding.connectionId || installationId !== binding.installationId) {
391
+ throw new Error('Zerops source transport binding has different application coordinates')
392
+ }
393
+ }
394
+
395
+ const resolveSource = async (
396
+ input: ProviderSourceResolutionInput,
397
+ binding?: ZeropsSourceTransportBinding,
398
+ ): Promise<ProviderSourceResolution> => {
399
+ assertSourceBinding(input, binding)
400
+ const registration = normalizeRegistration({ app: input.app, environment: input.environment })
401
+ const artifact = decodeEnvelope('artifact', registration.environment.artifact, zeropsArtifactCodec)
402
+ await verifyZeropsArtifactSourceDescriptor(artifact.target.sourceDescriptor)
403
+ const base = {
404
+ runId: input.runId,
405
+ repository: normalizeZeropsSourceRepository(registration.app.source.repoUrl),
406
+ requestedRef: registration.app.source.ref,
407
+ ...(input.expectedCommitSha === undefined ? {} : { expectedCommitSha: input.expectedCommitSha }),
408
+ descriptorSha256: artifact.target.sourceDescriptor.sha256,
409
+ signal: input.signal,
410
+ }
411
+ let result: ZeropsSourceResolveResult
412
+ if (binding?.transportKind === 'keyed-v2') {
413
+ const resolveV2 = options.source.resolveV2
414
+ if (resolveV2 === undefined) throw new Error('Zerops keyed source transport is unavailable')
415
+ result = await resolveV2.call(options.source, {
416
+ ...base,
417
+ privateBinding: { connectionId: binding.connectionId, installationId: binding.installationId },
418
+ })
419
+ } else {
420
+ result = await options.source.resolve({
421
+ ...base,
422
+ ...(binding === undefined ? {} : { githubInstallationId: binding.installationId }),
423
+ })
424
+ }
425
+ if (
426
+ result.runId !== input.runId
427
+ || result.descriptorSha256 !== artifact.target.sourceDescriptor.sha256
428
+ || (input.expectedCommitSha !== undefined && result.commitSha !== input.expectedCommitSha)
429
+ ) throw new Error('Zerops source resolution returned different coordinates')
430
+ return { commitSha: result.commitSha }
431
+ }
432
+
433
+ const controlProvider: ZeropsControlProvider = {
353
434
  id: 'zerops',
354
435
  normalizeRegistration,
355
- resolveSource: async (input) => {
356
- const registration = normalizeRegistration({ app: input.app, environment: input.environment })
357
- const artifact = decodeEnvelope('artifact', registration.environment.artifact, zeropsArtifactCodec)
358
- await verifyZeropsArtifactSourceDescriptor(artifact.target.sourceDescriptor)
359
- const repository = normalizeZeropsSourceRepository(registration.app.source.repoUrl)
360
- const result = await options.source.resolve({
361
- runId: input.runId,
362
- repository,
363
- requestedRef: registration.app.source.ref,
364
- ...(input.expectedCommitSha === undefined ? {} : { expectedCommitSha: input.expectedCommitSha }),
365
- ...(registration.app.source.githubInstallationId === undefined
366
- ? {}
367
- : { githubInstallationId: registration.app.source.githubInstallationId }),
368
- descriptorSha256: artifact.target.sourceDescriptor.sha256,
369
- signal: input.signal,
370
- })
371
- if (
372
- result.runId !== input.runId
373
- || result.descriptorSha256 !== artifact.target.sourceDescriptor.sha256
374
- || (input.expectedCommitSha !== undefined && result.commitSha !== input.expectedCommitSha)
375
- ) {
376
- throw new Error('Zerops source resolution returned different coordinates')
377
- }
378
- return { commitSha: result.commitSha }
379
- },
436
+ resolveSource: (input) => resolveSource(input),
437
+ resolveSourceWithBinding: (input) => resolveSource(input, input.sourceBinding),
380
438
  deploy: async (input: ProviderDeployInput): Promise<ProviderTerminalOutcome> => {
439
+ assertSourceBinding(input, sourceBindings.get(input.runId))
381
440
  const registration = normalizeRegistration({ app: input.app, environment: input.environment })
382
441
  const placement = resolvedEnvironment(registration.environment, { requireReady: true })
383
442
  const proxyServiceId = placement.proxyServiceId
@@ -469,6 +528,17 @@ export const createZeropsControlProvider = (options: ZeropsControlProviderOption
469
528
  artifact: registration.environment.artifact,
470
529
  })
471
530
  },
531
+ deployWithBinding: async (input) => {
532
+ const { sourceBinding, ...deployInput } = input
533
+ assertSourceBinding(deployInput, sourceBinding)
534
+ if (sourceBindings.has(input.runId)) throw new Error('Zerops source transport binding is already active for this run')
535
+ sourceBindings.set(input.runId, sourceBinding)
536
+ try {
537
+ return await controlProvider.deploy(deployInput)
538
+ } finally {
539
+ if (sourceBindings.get(input.runId) === sourceBinding) sourceBindings.delete(input.runId)
540
+ }
541
+ },
472
542
  cancel: async (input) => {
473
543
  resolvedEnvironment(input.environment, { requireReady: false })
474
544
  const state = parseRunState(input.providerState, input.externalId)
@@ -646,4 +716,5 @@ export const createZeropsControlProvider = (options: ZeropsControlProviderOption
646
716
  },
647
717
  }),
648
718
  }
719
+ return controlProvider
649
720
  }
package/src/index.ts CHANGED
@@ -98,54 +98,82 @@ export {
98
98
  buildZeropsSourceCancelRequest,
99
99
  buildZeropsSourceCancelResponse,
100
100
  buildZeropsSourceCredentialActivateRequest,
101
+ buildZeropsSourceCredentialActivateRequestV2,
101
102
  buildZeropsSourceCredentialActivateResponse,
103
+ buildZeropsSourceCredentialActivateResponseV2,
102
104
  buildZeropsSourceCredentialBundle,
105
+ buildZeropsSourceCredentialBundleV2,
103
106
  buildZeropsSourceCredentialStatusRequest,
107
+ buildZeropsSourceCredentialStatusRequestV2,
104
108
  buildZeropsSourceCredentialStatusResponse,
109
+ buildZeropsSourceCredentialStatusResponseV2,
105
110
  buildZeropsSourceErrorEnvelope,
106
111
  buildZeropsSourceInstallationsVerifyRequest,
107
112
  buildZeropsSourceInstallationsVerifyResponse,
108
113
  buildZeropsSourceResolveInstallationRequest,
109
114
  buildZeropsSourceResolveInstallationResponse,
110
115
  buildZeropsSourceResolveRequest,
116
+ buildZeropsSourceResolveRequestV2,
111
117
  buildZeropsSourceResolveResponse,
118
+ buildZeropsSourceResolveResponseV2,
112
119
  buildZeropsSourceUploadRequest,
120
+ buildZeropsSourceUploadRequestV2,
113
121
  buildZeropsSourceUploadResponse,
122
+ buildZeropsSourceUploadResponseV2,
114
123
  buildZeropsSourceWebhookConfigureRequest,
115
124
  buildZeropsSourceWebhookConfigureResponse,
116
125
  decodeZeropsSourceCancelRequest,
117
126
  decodeZeropsSourceCancelResponse,
118
127
  decodeZeropsSourceCredentialActivateRequest,
128
+ decodeZeropsSourceCredentialActivateRequestV2,
119
129
  decodeZeropsSourceCredentialActivateResponse,
130
+ decodeZeropsSourceCredentialActivateResponseV2,
120
131
  decodeZeropsSourceCredentialBundle,
132
+ decodeZeropsSourceCredentialBundleV2,
121
133
  decodeZeropsSourceCredentialStatusRequest,
134
+ decodeZeropsSourceCredentialStatusRequestV2,
122
135
  decodeZeropsSourceCredentialStatusResponse,
136
+ decodeZeropsSourceCredentialStatusResponseV2,
123
137
  decodeZeropsSourceErrorEnvelope,
124
138
  decodeZeropsSourceInstallationsVerifyRequest,
125
139
  decodeZeropsSourceInstallationsVerifyResponse,
126
140
  decodeZeropsSourceResolveInstallationRequest,
127
141
  decodeZeropsSourceResolveInstallationResponse,
128
142
  decodeZeropsSourceResolveRequest,
143
+ decodeZeropsSourceResolveRequestV2,
129
144
  decodeZeropsSourceResolveResponse,
145
+ decodeZeropsSourceResolveResponseV2,
130
146
  decodeZeropsSourceUploadRequest,
147
+ decodeZeropsSourceUploadRequestV2,
131
148
  decodeZeropsSourceUploadResponse,
149
+ decodeZeropsSourceUploadResponseV2,
132
150
  decodeZeropsSourceWebhookConfigureRequest,
133
151
  decodeZeropsSourceWebhookConfigureResponse,
134
152
  normalizeZeropsSourceRepository,
135
153
  serializeZeropsSourceCredentialBundle,
154
+ serializeZeropsSourceCredentialBundleV2,
136
155
  sha256ZeropsSourceCredentialBundle,
156
+ sha256ZeropsSourceCredentialBundleV2,
137
157
  ZEROPS_SOURCE_CANCEL_PATH,
138
158
  ZEROPS_SOURCE_CREDENTIAL_ACTIVATE_PATH,
159
+ ZEROPS_SOURCE_CREDENTIAL_ACTIVATE_PATH_V2,
139
160
  ZEROPS_SOURCE_CREDENTIAL_BUNDLE_VERSION,
161
+ ZEROPS_SOURCE_CREDENTIAL_BUNDLE_VERSION_V2,
162
+ ZEROPS_SOURCE_CREDENTIAL_ENV_V2_PREFIX,
140
163
  ZEROPS_SOURCE_CREDENTIAL_STATUS_PATH,
164
+ ZEROPS_SOURCE_CREDENTIAL_STATUS_PATH_V2,
141
165
  ZEROPS_SOURCE_ERROR_CODES,
142
166
  ZEROPS_SOURCE_ERROR_STAGES,
143
167
  ZEROPS_SOURCE_INSTALLATIONS_VERIFY_PATH,
144
168
  ZEROPS_SOURCE_PROTOCOL_VERSION,
169
+ ZEROPS_SOURCE_PROTOCOL_VERSION_V2,
145
170
  ZEROPS_SOURCE_RESOLVE_INSTALLATION_PATH,
146
171
  ZEROPS_SOURCE_RESOLVE_PATH,
172
+ ZEROPS_SOURCE_RESOLVE_PATH_V2,
147
173
  ZEROPS_SOURCE_UPLOAD_PATH,
174
+ ZEROPS_SOURCE_UPLOAD_PATH_V2,
148
175
  ZEROPS_SOURCE_WEBHOOK_CONFIGURE_PATH,
176
+ zeropsSourceCredentialEnvV2,
149
177
  } from './source'
150
178
  export type {
151
179
  ZeropsSourceCancelInput,
@@ -153,16 +181,25 @@ export type {
153
181
  ZeropsSourceCancelResponseV1,
154
182
  ZeropsSourceCancelResult,
155
183
  ZeropsSourceClient,
184
+ ZeropsSourceClientV2,
156
185
  ZeropsSourceCredentialActivateInput,
157
186
  ZeropsSourceCredentialActivateRequestV1,
187
+ ZeropsSourceCredentialActivateRequestV2,
158
188
  ZeropsSourceCredentialActivateResponseV1,
189
+ ZeropsSourceCredentialActivateResponseV2,
159
190
  ZeropsSourceCredentialActiveStatusResponseV1,
191
+ ZeropsSourceCredentialActiveStatusResponseV2,
160
192
  ZeropsSourceCredentialAnonymousStatusResponseV1,
193
+ ZeropsSourceCredentialAnonymousStatusResponseV2,
161
194
  ZeropsSourceCredentialBundleV1,
195
+ ZeropsSourceCredentialBundleV2,
162
196
  ZeropsSourceCredentialManager,
197
+ ZeropsSourceCredentialManagerV2,
163
198
  ZeropsSourceCredentialStatusInput,
164
199
  ZeropsSourceCredentialStatusRequestV1,
200
+ ZeropsSourceCredentialStatusRequestV2,
165
201
  ZeropsSourceCredentialStatusResponseV1,
202
+ ZeropsSourceCredentialStatusResponseV2,
166
203
  ZeropsSourceDescriptor,
167
204
  ZeropsSourceErrorCode,
168
205
  ZeropsSourceErrorEnvelope,
@@ -173,16 +210,23 @@ export type {
173
210
  ZeropsSourceInstallationsVerifyRequestV1,
174
211
  ZeropsSourceInstallationsVerifyResponseV1,
175
212
  ZeropsSourceInstallationVerificationV1,
213
+ ZeropsSourcePrivateBindingV2,
176
214
  ZeropsSourceRepository,
177
215
  ZeropsSourceResolveInput,
216
+ ZeropsSourceResolveInputV2,
178
217
  ZeropsSourceResolveInstallationRequestV1,
179
218
  ZeropsSourceResolveInstallationResponseV1,
180
219
  ZeropsSourceResolveRequestV1,
220
+ ZeropsSourceResolveRequestV2,
181
221
  ZeropsSourceResolveResponseV1,
222
+ ZeropsSourceResolveResponseV2,
182
223
  ZeropsSourceResolveResult,
183
224
  ZeropsSourceUploadInput,
225
+ ZeropsSourceUploadInputV2,
184
226
  ZeropsSourceUploadRequestV1,
227
+ ZeropsSourceUploadRequestV2,
185
228
  ZeropsSourceUploadResponseV1,
229
+ ZeropsSourceUploadResponseV2,
186
230
  ZeropsSourceUploadResult,
187
231
  ZeropsSourceWebhookConfigureInput,
188
232
  ZeropsSourceWebhookConfigureRequestV1,
package/src/provider.ts CHANGED
@@ -30,12 +30,24 @@ import {
30
30
  zeropsArtifactCodec,
31
31
  } from './manifest'
32
32
  import { buildZeropsPlan, resolveDeployHostname, type ZeropsJobSpec, type ZeropsPlan } from './plan'
33
+ import type { ZeropsSourceUploadInputV2, ZeropsSourceUploadResult } from './source'
33
34
  import type { ZeropsRunState, ZeropsRuntimeSource, ZeropsRuntimeTarget } from './types'
34
35
 
35
36
  export const CANCELLED = 'deploy cancelled'
36
37
 
37
38
  type ZeropsRun = TypedProviderRun<ZeropsRuntimeTarget, FabrikaManifest>
38
39
 
40
+ export interface ZeropsSourceTransportBinding {
41
+ readonly connectionId: string
42
+ readonly installationId: number
43
+ readonly transportKind: 'legacy-v1' | 'keyed-v2'
44
+ }
45
+
46
+ export interface ZeropsSourceTransportRouting {
47
+ bindingForRun(runId: string): ZeropsSourceTransportBinding | undefined
48
+ uploadV2(input: ZeropsSourceUploadInputV2): Promise<ZeropsSourceUploadResult>
49
+ }
50
+
39
51
  /** How often `await-deploy` asks Zerops for the version's status. */
40
52
  const POLL_INTERVAL_MS = 3000
41
53
 
@@ -94,6 +106,7 @@ interface StepEnv {
94
106
  log: (line: string) => void
95
107
  signal: AbortSignal
96
108
  dryRun: boolean
109
+ sourceTransport?: ZeropsSourceTransportRouting
97
110
  /** Set by `trigger-deploy`, read by `await-deploy` — the only state that crosses a step boundary. */
98
111
  state: { appVersionId?: string; processId?: string }
99
112
  }
@@ -276,7 +289,7 @@ const runStep = async (spec: ZeropsJobSpec, env: StepEnv): Promise<void> => {
276
289
  let buildTriggerRequested = false
277
290
  try {
278
291
  await run.events.externalId(version.id, runState(version.id, 'version_created'))
279
- const uploaded = await source.client.upload({
292
+ const uploadInput = {
280
293
  runId: source.runtime.runId,
281
294
  appVersionId: version.id,
282
295
  repository: source.runtime.repository,
@@ -290,7 +303,22 @@ const runStep = async (spec: ZeropsJobSpec, env: StepEnv): Promise<void> => {
290
303
  sha256: artifact.target.sourceDescriptor.sha256,
291
304
  },
292
305
  signal,
293
- })
306
+ }
307
+ const sourceTransport = env.sourceTransport
308
+ const binding = sourceTransport?.bindingForRun(source.runtime.runId)
309
+ if (binding !== undefined && source.runtime.githubInstallationId !== binding.installationId) {
310
+ throw new Error('zerops: source transport binding has different installation coordinates')
311
+ }
312
+ let uploaded: ZeropsSourceUploadResult
313
+ if (binding?.transportKind === 'keyed-v2') {
314
+ if (sourceTransport === undefined) throw new Error('zerops: keyed source transport is unavailable')
315
+ uploaded = await sourceTransport.uploadV2({
316
+ ...uploadInput,
317
+ privateBinding: { connectionId: binding.connectionId, installationId: binding.installationId },
318
+ })
319
+ } else {
320
+ uploaded = await source.client.upload(uploadInput)
321
+ }
294
322
  if (
295
323
  uploaded.runId !== source.runtime.runId
296
324
  || uploaded.appVersionId !== version.id
@@ -404,6 +432,7 @@ export type ZeropsProvider = ProviderModule<'zerops', ZeropsRuntimeTarget, Fabri
404
432
  /** Construct an independently testable Zerops provider against one collaborator factory. */
405
433
  export const createZeropsProvider = (
406
434
  collaborators: ZeropsCollaboratorFactory = defaultZeropsCollaborators,
435
+ sourceTransport?: ZeropsSourceTransportRouting,
407
436
  ): ZeropsProvider =>
408
437
  createProvider({
409
438
  id: 'zerops',
@@ -431,6 +460,7 @@ export const createZeropsProvider = (
431
460
  log: run.events.log,
432
461
  signal: run.signal,
433
462
  dryRun: run.dryRun,
463
+ ...(sourceTransport === undefined ? {} : { sourceTransport }),
434
464
  state: {},
435
465
  }
436
466
  const byId = new Map(plan.steps.map((step): [string, ZeropsJobSpec] => [step.id, step]))
@@ -2,11 +2,19 @@ import type { ZeropsApi, ZeropsService, ZeropsServiceEnv } from './api'
2
2
  import {
3
3
  buildZeropsSourceCredentialBundle,
4
4
  decodeZeropsSourceCredentialBundle,
5
+ decodeZeropsSourceCredentialBundleV2,
5
6
  serializeZeropsSourceCredentialBundle,
6
7
  sha256ZeropsSourceCredentialBundle,
8
+ sha256ZeropsSourceCredentialBundleV2,
9
+ ZEROPS_SOURCE_CREDENTIAL_BUNDLE_VERSION_V2,
10
+ ZEROPS_SOURCE_PROTOCOL_VERSION_V2,
7
11
  type ZeropsSourceCredentialActivateResponseV1,
12
+ type ZeropsSourceCredentialActivateResponseV2,
13
+ zeropsSourceCredentialEnvV2,
8
14
  type ZeropsSourceCredentialManager,
15
+ type ZeropsSourceCredentialManagerV2,
9
16
  type ZeropsSourceCredentialStatusResponseV1,
17
+ type ZeropsSourceCredentialStatusResponseV2,
10
18
  type ZeropsSourceGitHubAppIdentityV1,
11
19
  type ZeropsSourceInstallationsVerifyInput,
12
20
  type ZeropsSourceInstallationsVerifyResponseV1,
@@ -19,7 +27,6 @@ export const ZEROPS_SOURCE_LEGACY_APP_ID_ENV = 'GITHUB_APP_ID'
19
27
  export const ZEROPS_SOURCE_LEGACY_PRIVATE_KEY_ENV = 'GITHUB_APP_PRIVATE_KEY'
20
28
 
21
29
  const SOURCE_HOSTNAME = 'source'
22
- const MAX_ENV_ENTRIES = 256
23
30
  const PROJECT_ID_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/
24
31
  const DEFAULT_REREAD_ATTEMPTS = 5
25
32
  const DEFAULT_REREAD_DELAY_MS = 250
@@ -62,16 +69,23 @@ export interface SourceConnectionAdmin {
62
69
  inspect(signal: AbortSignal): Promise<SourceConnectionInspection>
63
70
  adoptExisting(input: SourceConnectionAdoptExistingInput): Promise<ZeropsSourceCredentialActivateResponseV1>
64
71
  activate(input: SourceConnectionActivateInput): Promise<ZeropsSourceCredentialActivateResponseV1>
72
+ activateV2?(input: SourceConnectionActivateInput): Promise<ZeropsSourceCredentialActivateResponseV2>
65
73
  status(input: SourceConnectionStatusInput): Promise<SourceConnectionStatus>
74
+ statusV2?(input: SourceConnectionStatusInput): Promise<SourceConnectionStatus>
66
75
  configureWebhook(input: ZeropsSourceWebhookConfigureInput): Promise<ZeropsSourceWebhookConfigureResponseV1>
67
76
  verifyInstallations(input: ZeropsSourceInstallationsVerifyInput): Promise<ZeropsSourceInstallationsVerifyResponseV1>
68
77
  }
69
78
 
79
+ interface CompleteSourceConnectionAdmin extends SourceConnectionAdmin {
80
+ activateV2(input: SourceConnectionActivateInput): Promise<ZeropsSourceCredentialActivateResponseV2>
81
+ statusV2(input: SourceConnectionStatusInput): Promise<SourceConnectionStatus>
82
+ }
83
+
70
84
  export type SourceConnectionZeropsApi = Pick<ZeropsApi, 'findService' | 'listServiceEnv' | 'createServiceEnv'>
71
85
 
72
86
  export interface ZeropsSourceConnectionAdminOptions {
73
87
  readonly api: SourceConnectionZeropsApi
74
- readonly source: ZeropsSourceCredentialManager
88
+ readonly source: ZeropsSourceCredentialManager & Partial<ZeropsSourceCredentialManagerV2>
75
89
  readonly projectId: string
76
90
  readonly reread?: {
77
91
  readonly attempts?: number
@@ -102,7 +116,16 @@ interface DurableInspection {
102
116
  readonly bundle?: string
103
117
  }
104
118
 
105
- export function createZeropsSourceConnectionAdmin(options: ZeropsSourceConnectionAdminOptions): SourceConnectionAdmin {
119
+ interface DurableInspectionV2 {
120
+ readonly public:
121
+ | { readonly state: 'anonymous' }
122
+ | { readonly state: 'durable'; readonly credentialSha256: string }
123
+ readonly service: ZeropsService
124
+ readonly environmentKey: string
125
+ readonly bundle?: string
126
+ }
127
+
128
+ export function createZeropsSourceConnectionAdmin(options: ZeropsSourceConnectionAdminOptions): CompleteSourceConnectionAdmin {
106
129
  if (!PROJECT_ID_PATTERN.test(options.projectId)) throw new SourceConnectionAdminError('invalid_configuration')
107
130
  const attempts = options.reread?.attempts ?? DEFAULT_REREAD_ATTEMPTS
108
131
  const delayMs = options.reread?.delayMs ?? DEFAULT_REREAD_DELAY_MS
@@ -111,7 +134,7 @@ export function createZeropsSourceConnectionAdmin(options: ZeropsSourceConnectio
111
134
  }
112
135
  const sleep = options.reread?.sleep ?? defaultSleep
113
136
 
114
- const inspectInternal = async (signal: AbortSignal): Promise<DurableInspection> => {
137
+ const readEnvironment = async (signal: AbortSignal): Promise<{ service: ZeropsService; environment: ZeropsServiceEnv[] }> => {
115
138
  throwIfAborted(signal)
116
139
  const service = await exactSourceService(options.api, options.projectId, signal)
117
140
  let environment: ZeropsServiceEnv[]
@@ -122,10 +145,26 @@ export function createZeropsSourceConnectionAdmin(options: ZeropsSourceConnectio
122
145
  throw new SourceConnectionAdminError('credential_persistence')
123
146
  }
124
147
  throwIfAborted(signal)
125
- return await withDigest(classify(service, environment))
148
+ return { service, environment }
126
149
  }
127
150
 
128
- const admin: SourceConnectionAdmin = {
151
+ const inspectInternal = async (signal: AbortSignal): Promise<DurableInspection> => {
152
+ const current = await readEnvironment(signal)
153
+ return await withDigest(classify(current.service, current.environment))
154
+ }
155
+
156
+ const inspectV2Internal = async (connectionId: string, signal: AbortSignal): Promise<DurableInspectionV2> => {
157
+ let environmentKey: string
158
+ try {
159
+ environmentKey = await zeropsSourceCredentialEnvV2(connectionId)
160
+ } catch {
161
+ throw new SourceConnectionAdminError('credential_conflict')
162
+ }
163
+ const current = await readEnvironment(signal)
164
+ return await classifyV2(current.service, current.environment, environmentKey, connectionId)
165
+ }
166
+
167
+ const admin: CompleteSourceConnectionAdmin = {
129
168
  inspect: async (signal) => (await inspectInternal(signal)).public,
130
169
 
131
170
  adoptExisting: async (input) => {
@@ -212,6 +251,68 @@ export function createZeropsSourceConnectionAdmin(options: ZeropsSourceConnectio
212
251
  return activated
213
252
  },
214
253
 
254
+ activateV2: async (input) => {
255
+ if (options.source.activateV2 === undefined || options.source.statusV2 === undefined) {
256
+ throw new SourceConnectionAdminError('credential_activation')
257
+ }
258
+ const activateV2 = options.source.activateV2.bind(options.source)
259
+ const statusV2 = options.source.statusV2.bind(options.source)
260
+ let digest: string
261
+ try {
262
+ const bundle = decodeZeropsSourceCredentialBundleV2(input.credentialBundle)
263
+ if (bundle.connectionId !== input.connectionId) throw new Error('connection mismatch')
264
+ digest = await sha256ZeropsSourceCredentialBundleV2(input.credentialBundle)
265
+ } catch {
266
+ throw new SourceConnectionAdminError('credential_conflict')
267
+ }
268
+ if (digest !== input.credentialSha256) throw new SourceConnectionAdminError('credential_conflict')
269
+ const initial = await inspectV2Internal(input.connectionId, input.signal)
270
+ if (initial.public.state === 'durable') {
271
+ if (initial.bundle !== input.credentialBundle || initial.public.credentialSha256 !== digest) {
272
+ throw new SourceConnectionAdminError('credential_conflict')
273
+ }
274
+ } else {
275
+ try {
276
+ await options.api.createServiceEnv({
277
+ serviceId: initial.service.id,
278
+ key: initial.environmentKey,
279
+ value: input.credentialBundle,
280
+ signal: input.signal,
281
+ })
282
+ } catch (error) {
283
+ if (isAbort(error, input.signal)) throw error
284
+ }
285
+ await proveDurableV2(input, digest, attempts, delayMs, sleep, inspectV2Internal)
286
+ }
287
+ let activated: ZeropsSourceCredentialActivateResponseV2
288
+ try {
289
+ activated = await activateV2(input)
290
+ } catch (error) {
291
+ if (isAbort(error, input.signal)) throw error
292
+ let status: ZeropsSourceCredentialStatusResponseV2
293
+ try {
294
+ status = await statusV2({ connectionId: input.connectionId, signal: input.signal })
295
+ } catch (statusError) {
296
+ if (isAbort(statusError, input.signal)) throw statusError
297
+ throw new SourceConnectionAdminError('credential_activation')
298
+ }
299
+ if (
300
+ status.state !== 'active' || status.connectionId !== input.connectionId || status.credentialSha256 !== digest
301
+ ) throw new SourceConnectionAdminError('credential_activation')
302
+ activated = {
303
+ protocolVersion: ZEROPS_SOURCE_PROTOCOL_VERSION_V2,
304
+ connectionId: input.connectionId,
305
+ credentialVersion: ZEROPS_SOURCE_CREDENTIAL_BUNDLE_VERSION_V2,
306
+ credentialSha256: status.credentialSha256,
307
+ githubApp: status.githubApp,
308
+ }
309
+ }
310
+ if (activated.connectionId !== input.connectionId || activated.credentialSha256 !== digest) {
311
+ throw new SourceConnectionAdminError('credential_activation')
312
+ }
313
+ return activated
314
+ },
315
+
215
316
  status: async (input) => {
216
317
  const durable = await inspectInternal(input.signal)
217
318
  if (durable.public.state !== 'durable') return durable.public
@@ -231,8 +332,30 @@ export function createZeropsSourceConnectionAdmin(options: ZeropsSourceConnectio
231
332
  return { state: 'active', credentialSha256: runtime.credentialSha256, githubApp: runtime.githubApp }
232
333
  },
233
334
 
335
+ statusV2: async (input) => {
336
+ const durable = await inspectV2Internal(input.connectionId, input.signal)
337
+ if (durable.public.state !== 'durable') return durable.public
338
+ const statusV2 = options.source.statusV2
339
+ if (statusV2 === undefined) throw new SourceConnectionAdminError('credential_activation')
340
+ const readStatusV2 = statusV2.bind(options.source)
341
+ let runtime: ZeropsSourceCredentialStatusResponseV2
342
+ try {
343
+ runtime = await readStatusV2(input)
344
+ } catch (error) {
345
+ if (isAbort(error, input.signal)) throw error
346
+ throw new SourceConnectionAdminError('credential_activation')
347
+ }
348
+ if (runtime.connectionId !== input.connectionId || runtime.state === 'anonymous') {
349
+ return { state: 'activation-required', credentialSha256: durable.public.credentialSha256 }
350
+ }
351
+ if (runtime.credentialSha256 !== durable.public.credentialSha256) {
352
+ throw new SourceConnectionAdminError('credential_conflict')
353
+ }
354
+ return { state: 'active', credentialSha256: runtime.credentialSha256, githubApp: runtime.githubApp }
355
+ },
356
+
234
357
  configureWebhook: async (input) => {
235
- await requireDurableDigest(input.credentialSha256, input.signal, inspectInternal)
358
+ await requireDurableDigest(input.connectionId, input.credentialSha256, input.signal, inspectInternal, inspectV2Internal)
236
359
  try {
237
360
  const response = await options.source.configureWebhook(input)
238
361
  if (
@@ -248,7 +371,7 @@ export function createZeropsSourceConnectionAdmin(options: ZeropsSourceConnectio
248
371
  },
249
372
 
250
373
  verifyInstallations: async (input) => {
251
- await requireDurableDigest(input.credentialSha256, input.signal, inspectInternal)
374
+ await requireDurableDigest(input.connectionId, input.credentialSha256, input.signal, inspectInternal, inspectV2Internal)
252
375
  try {
253
376
  const response = await options.source.verifyInstallations(input)
254
377
  if (response.connectionId !== input.connectionId || response.credentialSha256 !== input.credentialSha256) {
@@ -272,10 +395,17 @@ async function adoptionConnectionId(projectId: string, credentialSha256: string)
272
395
  }
273
396
 
274
397
  async function requireDurableDigest(
398
+ connectionId: string,
275
399
  digest: string,
276
400
  signal: AbortSignal,
277
401
  inspect: (signal: AbortSignal) => Promise<DurableInspection>,
402
+ inspectV2: (connectionId: string, signal: AbortSignal) => Promise<DurableInspectionV2>,
278
403
  ): Promise<void> {
404
+ const keyed = await inspectV2(connectionId, signal)
405
+ if (keyed.public.state === 'durable') {
406
+ if (keyed.public.credentialSha256 === digest) return
407
+ throw new SourceConnectionAdminError('credential_conflict')
408
+ }
279
409
  const durable = await inspect(signal)
280
410
  if (durable.public.state !== 'durable' || durable.public.credentialSha256 !== digest) {
281
411
  throw new SourceConnectionAdminError('credential_conflict')
@@ -298,7 +428,6 @@ async function exactSourceService(api: SourceConnectionZeropsApi, projectId: str
298
428
  }
299
429
 
300
430
  function classify(service: ZeropsService, environment: readonly ZeropsServiceEnv[]): DurableInspection {
301
- if (environment.length > MAX_ENV_ENTRIES) throw new SourceConnectionAdminError('source_mismatch')
302
431
  const bundleEntries = environment.filter((entry) => entry.key === ZEROPS_SOURCE_CREDENTIAL_ENV)
303
432
  const legacyIdEntries = environment.filter((entry) => entry.key === ZEROPS_SOURCE_LEGACY_APP_ID_ENV)
304
433
  const legacyKeyEntries = environment.filter((entry) => entry.key === ZEROPS_SOURCE_LEGACY_PRIVATE_KEY_ENV)
@@ -337,6 +466,31 @@ function classify(service: ZeropsService, environment: readonly ZeropsServiceEnv
337
466
  return { public: { state: 'legacy-partial' }, service }
338
467
  }
339
468
 
469
+ async function classifyV2(
470
+ service: ZeropsService,
471
+ environment: readonly ZeropsServiceEnv[],
472
+ environmentKey: string,
473
+ connectionId: string,
474
+ ): Promise<DurableInspectionV2> {
475
+ const entries = environment.filter((entry) => entry.key === environmentKey)
476
+ if (entries.length > 1) throw new SourceConnectionAdminError('credential_conflict')
477
+ if (entries.length === 0) return { public: { state: 'anonymous' }, service, environmentKey }
478
+ const bundle = entries[0]?.content
479
+ if (bundle === undefined) throw new SourceConnectionAdminError('credential_conflict')
480
+ try {
481
+ const decoded = decodeZeropsSourceCredentialBundleV2(bundle)
482
+ if (decoded.connectionId !== connectionId) throw new Error('connection mismatch')
483
+ return {
484
+ public: { state: 'durable', credentialSha256: await sha256ZeropsSourceCredentialBundleV2(bundle) },
485
+ service,
486
+ environmentKey,
487
+ bundle,
488
+ }
489
+ } catch {
490
+ throw new SourceConnectionAdminError('credential_conflict')
491
+ }
492
+ }
493
+
340
494
  async function withDigest(inspection: DurableInspection): Promise<DurableInspection> {
341
495
  if (inspection.public.state !== 'durable' || inspection.bundle === undefined) return inspection
342
496
  return {
@@ -366,6 +520,25 @@ async function proveDurable(
366
520
  throw new SourceConnectionAdminError('credential_persistence')
367
521
  }
368
522
 
523
+ async function proveDurableV2(
524
+ input: SourceConnectionActivateInput,
525
+ digest: string,
526
+ attempts: number,
527
+ delayMs: number,
528
+ sleep: (delayMs: number, signal: AbortSignal) => Promise<void>,
529
+ inspect: (connectionId: string, signal: AbortSignal) => Promise<DurableInspectionV2>,
530
+ ): Promise<void> {
531
+ for (let attempt = 0; attempt < attempts; attempt++) {
532
+ const durable = await inspect(input.connectionId, input.signal)
533
+ if (durable.public.state === 'durable') {
534
+ if (durable.bundle === input.credentialBundle && durable.public.credentialSha256 === digest) return
535
+ throw new SourceConnectionAdminError('credential_conflict')
536
+ }
537
+ if (attempt + 1 < attempts) await sleep(delayMs, input.signal)
538
+ }
539
+ throw new SourceConnectionAdminError('credential_persistence')
540
+ }
541
+
369
542
  const defaultSleep = (delayMs: number, signal: AbortSignal): Promise<void> => {
370
543
  throwIfAborted(signal)
371
544
  return new Promise((resolve, reject) => {
package/src/source.ts CHANGED
@@ -8,6 +8,13 @@ export const ZEROPS_SOURCE_CREDENTIAL_STATUS_PATH = '/v1/source/credentials/stat
8
8
  export const ZEROPS_SOURCE_WEBHOOK_CONFIGURE_PATH = '/v1/source/github/webhook/configure'
9
9
  export const ZEROPS_SOURCE_INSTALLATIONS_VERIFY_PATH = '/v1/source/github/installations/verify'
10
10
  export const ZEROPS_SOURCE_CREDENTIAL_BUNDLE_VERSION = 1
11
+ export const ZEROPS_SOURCE_PROTOCOL_VERSION_V2 = 2
12
+ export const ZEROPS_SOURCE_RESOLVE_PATH_V2 = '/v2/source/resolve'
13
+ export const ZEROPS_SOURCE_UPLOAD_PATH_V2 = '/v2/source/upload'
14
+ export const ZEROPS_SOURCE_CREDENTIAL_ACTIVATE_PATH_V2 = '/v2/source/credentials/activate'
15
+ export const ZEROPS_SOURCE_CREDENTIAL_STATUS_PATH_V2 = '/v2/source/credentials/status'
16
+ export const ZEROPS_SOURCE_CREDENTIAL_BUNDLE_VERSION_V2 = 2
17
+ export const ZEROPS_SOURCE_CREDENTIAL_ENV_V2_PREFIX = 'GITHUB_APP_CREDENTIALS_V2_'
11
18
 
12
19
  const MAX_ID_LENGTH = 128
13
20
  const MAX_REF_LENGTH = 255
@@ -32,6 +39,13 @@ export interface ZeropsSourceCredentialBundleV1 {
32
39
  readonly privateKeyPem: string
33
40
  }
34
41
 
42
+ export interface ZeropsSourceCredentialBundleV2 {
43
+ readonly version: typeof ZEROPS_SOURCE_CREDENTIAL_BUNDLE_VERSION_V2
44
+ readonly connectionId: string
45
+ readonly githubAppId: string
46
+ readonly privateKeyPem: string
47
+ }
48
+
35
49
  export interface ZeropsSourceGitHubAppIdentityV1 {
36
50
  readonly id: number
37
51
  readonly slug: string
@@ -82,6 +96,41 @@ export type ZeropsSourceCredentialStatusResponseV1 =
82
96
  | ZeropsSourceCredentialAnonymousStatusResponseV1
83
97
  | ZeropsSourceCredentialActiveStatusResponseV1
84
98
 
99
+ export interface ZeropsSourceCredentialActivateRequestV2 {
100
+ readonly protocolVersion: typeof ZEROPS_SOURCE_PROTOCOL_VERSION_V2
101
+ readonly connectionId: string
102
+ /** Canonical JSON that is byte-identical to the durable source environment value. */
103
+ readonly credentialBundle: string
104
+ readonly credentialSha256: string
105
+ }
106
+
107
+ export interface ZeropsSourceCredentialActivateResponseV2 {
108
+ readonly protocolVersion: typeof ZEROPS_SOURCE_PROTOCOL_VERSION_V2
109
+ readonly connectionId: string
110
+ readonly credentialVersion: typeof ZEROPS_SOURCE_CREDENTIAL_BUNDLE_VERSION_V2
111
+ readonly credentialSha256: string
112
+ readonly githubApp: ZeropsSourceGitHubAppIdentityV1
113
+ }
114
+
115
+ export interface ZeropsSourceCredentialStatusRequestV2 {
116
+ readonly protocolVersion: typeof ZEROPS_SOURCE_PROTOCOL_VERSION_V2
117
+ readonly connectionId: string
118
+ }
119
+
120
+ export interface ZeropsSourceCredentialAnonymousStatusResponseV2 {
121
+ readonly protocolVersion: typeof ZEROPS_SOURCE_PROTOCOL_VERSION_V2
122
+ readonly connectionId: string
123
+ readonly state: 'anonymous'
124
+ }
125
+
126
+ export interface ZeropsSourceCredentialActiveStatusResponseV2 extends ZeropsSourceCredentialActivateResponseV2 {
127
+ readonly state: 'active'
128
+ }
129
+
130
+ export type ZeropsSourceCredentialStatusResponseV2 =
131
+ | ZeropsSourceCredentialAnonymousStatusResponseV2
132
+ | ZeropsSourceCredentialActiveStatusResponseV2
133
+
85
134
  export interface ZeropsSourceWebhookConfigureInput {
86
135
  readonly connectionId: string
87
136
  readonly credentialSha256: string
@@ -195,6 +244,47 @@ export interface ZeropsSourceUploadResponseV1 {
195
244
  descriptorSha256: string
196
245
  }
197
246
 
247
+ export interface ZeropsSourcePrivateBindingV2 {
248
+ readonly connectionId: string
249
+ readonly installationId: number
250
+ }
251
+
252
+ export interface ZeropsSourceResolveRequestV2 {
253
+ readonly protocolVersion: typeof ZEROPS_SOURCE_PROTOCOL_VERSION_V2
254
+ readonly runId: string
255
+ readonly repository: ZeropsSourceRepository
256
+ readonly requestedRef: string
257
+ readonly expectedCommitSha?: string
258
+ readonly privateBinding?: ZeropsSourcePrivateBindingV2
259
+ readonly descriptorSha256: string
260
+ }
261
+
262
+ export interface ZeropsSourceResolveResponseV2 {
263
+ readonly protocolVersion: typeof ZEROPS_SOURCE_PROTOCOL_VERSION_V2
264
+ readonly runId: string
265
+ readonly commitSha: string
266
+ readonly descriptorSha256: string
267
+ }
268
+
269
+ export interface ZeropsSourceUploadRequestV2 {
270
+ readonly protocolVersion: typeof ZEROPS_SOURCE_PROTOCOL_VERSION_V2
271
+ readonly runId: string
272
+ readonly appVersionId: string
273
+ readonly repository: ZeropsSourceRepository
274
+ readonly commitSha: string
275
+ readonly privateBinding?: ZeropsSourcePrivateBindingV2
276
+ readonly uploadUrl: string
277
+ readonly descriptor: ZeropsSourceDescriptor
278
+ }
279
+
280
+ export interface ZeropsSourceUploadResponseV2 {
281
+ readonly protocolVersion: typeof ZEROPS_SOURCE_PROTOCOL_VERSION_V2
282
+ readonly runId: string
283
+ readonly appVersionId: string
284
+ readonly commitSha: string
285
+ readonly descriptorSha256: string
286
+ }
287
+
198
288
  export interface ZeropsSourceCancelRequestV1 {
199
289
  protocolVersion: typeof ZEROPS_SOURCE_PROTOCOL_VERSION
200
290
  runId: string
@@ -303,6 +393,27 @@ export interface ZeropsSourceUploadInput {
303
393
  signal: AbortSignal
304
394
  }
305
395
 
396
+ export interface ZeropsSourceResolveInputV2 {
397
+ readonly runId: string
398
+ readonly repository: ZeropsSourceRepository
399
+ readonly requestedRef: string
400
+ readonly expectedCommitSha?: string
401
+ readonly privateBinding?: ZeropsSourcePrivateBindingV2
402
+ readonly descriptorSha256: string
403
+ readonly signal: AbortSignal
404
+ }
405
+
406
+ export interface ZeropsSourceUploadInputV2 {
407
+ readonly runId: string
408
+ readonly appVersionId: string
409
+ readonly repository: ZeropsSourceRepository
410
+ readonly commitSha: string
411
+ readonly privateBinding?: ZeropsSourcePrivateBindingV2
412
+ readonly uploadUrl: string
413
+ readonly descriptor: ZeropsSourceDescriptor
414
+ readonly signal: AbortSignal
415
+ }
416
+
306
417
  export interface ZeropsSourceUploadResult {
307
418
  runId: string
308
419
  appVersionId: string
@@ -323,6 +434,12 @@ export interface ZeropsSourceClient {
323
434
  cancel(input: ZeropsSourceCancelInput): Promise<void>
324
435
  }
325
436
 
437
+ /** Keyed source operations are additive so legacy client implementations remain valid. */
438
+ export interface ZeropsSourceClientV2 {
439
+ resolveV2(input: ZeropsSourceResolveInputV2): Promise<ZeropsSourceResolveResult>
440
+ uploadV2(input: ZeropsSourceUploadInputV2): Promise<ZeropsSourceUploadResult>
441
+ }
442
+
326
443
  export interface ZeropsSourceCredentialActivateInput {
327
444
  readonly connectionId: string
328
445
  readonly credentialBundle: string
@@ -343,6 +460,12 @@ export interface ZeropsSourceCredentialManager {
343
460
  verifyInstallations(input: ZeropsSourceInstallationsVerifyInput): Promise<ZeropsSourceInstallationsVerifyResponseV1>
344
461
  }
345
462
 
463
+ /** Keyed credential operations are additive so legacy credential managers remain valid. */
464
+ export interface ZeropsSourceCredentialManagerV2 {
465
+ activateV2(input: ZeropsSourceCredentialActivateInput): Promise<ZeropsSourceCredentialActivateResponseV2>
466
+ statusV2(input: ZeropsSourceCredentialStatusInput): Promise<ZeropsSourceCredentialStatusResponseV2>
467
+ }
468
+
346
469
  const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === 'object' && value !== null && !Array.isArray(value)
347
470
 
348
471
  const record = (value: unknown, label: string): Record<string, unknown> => {
@@ -363,6 +486,11 @@ const protocolVersion = (value: Record<string, unknown>, label: string): typeof
363
486
  return ZEROPS_SOURCE_PROTOCOL_VERSION
364
487
  }
365
488
 
489
+ const protocolVersionV2 = (value: Record<string, unknown>, label: string): typeof ZEROPS_SOURCE_PROTOCOL_VERSION_V2 => {
490
+ if (value['protocolVersion'] !== ZEROPS_SOURCE_PROTOCOL_VERSION_V2) throw new Error(`${label} has an unsupported protocolVersion`)
491
+ return ZEROPS_SOURCE_PROTOCOL_VERSION_V2
492
+ }
493
+
366
494
  const boundedString = (value: unknown, label: string, maximum: number): string => {
367
495
  if (typeof value !== 'string' || value.length === 0 || value.length > maximum) {
368
496
  throw new Error(`${label} must be a non-empty string of at most ${maximum} characters`)
@@ -428,6 +556,24 @@ const sourceCredentialBundleValue = (value: unknown): ZeropsSourceCredentialBund
428
556
  return { version: ZEROPS_SOURCE_CREDENTIAL_BUNDLE_VERSION, githubAppId, privateKeyPem }
429
557
  }
430
558
 
559
+ const sourceCredentialBundleValueV2 = (value: unknown): ZeropsSourceCredentialBundleV2 => {
560
+ const parsed = record(value, 'source credential bundle v2')
561
+ exactKeys(parsed, ['version', 'connectionId', 'githubAppId', 'privateKeyPem'], 'source credential bundle v2')
562
+ if (parsed['version'] !== ZEROPS_SOURCE_CREDENTIAL_BUNDLE_VERSION_V2) {
563
+ throw new Error('source credential bundle v2 has an unsupported version')
564
+ }
565
+ const connectionId = identifier(parsed['connectionId'], 'source credential bundle v2.connectionId')
566
+ const githubAppId = boundedString(parsed['githubAppId'], 'source credential bundle v2.githubAppId', 32)
567
+ if (!GITHUB_APP_ID_PATTERN.test(githubAppId)) throw new Error('source credential bundle v2.githubAppId is invalid')
568
+ const privateKeyPem = boundedString(
569
+ parsed['privateKeyPem'],
570
+ 'source credential bundle v2.privateKeyPem',
571
+ MAX_GITHUB_APP_PRIVATE_KEY_LENGTH,
572
+ )
573
+ if (!isCanonicalPrivateKeyPem(privateKeyPem)) throw new Error('source credential bundle v2.privateKeyPem is invalid')
574
+ return { version: ZEROPS_SOURCE_CREDENTIAL_BUNDLE_VERSION_V2, connectionId, githubAppId, privateKeyPem }
575
+ }
576
+
431
577
  const isCanonicalPrivateKeyPem = (value: string): boolean => {
432
578
  const match = /^-----BEGIN (RSA PRIVATE KEY|PRIVATE KEY)-----\n([A-Za-z0-9+/=\n]+)\n-----END (RSA PRIVATE KEY|PRIVATE KEY)-----\n?$/.exec(value)
433
579
  if (match === null || match[1] !== match[3]) return false
@@ -478,6 +624,51 @@ export async function sha256ZeropsSourceCredentialBundle(value: string): Promise
478
624
  return [...digest].map((byte) => byte.toString(16).padStart(2, '0')).join('')
479
625
  }
480
626
 
627
+ export function buildZeropsSourceCredentialBundleV2(
628
+ input: Omit<ZeropsSourceCredentialBundleV2, 'version'>,
629
+ ): ZeropsSourceCredentialBundleV2 {
630
+ return sourceCredentialBundleValueV2({ version: ZEROPS_SOURCE_CREDENTIAL_BUNDLE_VERSION_V2, ...input })
631
+ }
632
+
633
+ export function serializeZeropsSourceCredentialBundleV2(bundle: ZeropsSourceCredentialBundleV2): string {
634
+ const parsed = sourceCredentialBundleValueV2(bundle)
635
+ return JSON.stringify({
636
+ version: parsed.version,
637
+ connectionId: parsed.connectionId,
638
+ githubAppId: parsed.githubAppId,
639
+ privateKeyPem: parsed.privateKeyPem,
640
+ })
641
+ }
642
+
643
+ export function decodeZeropsSourceCredentialBundleV2(value: unknown): ZeropsSourceCredentialBundleV2 {
644
+ if (typeof value !== 'string' || new TextEncoder().encode(value).byteLength > MAX_SOURCE_CREDENTIAL_BUNDLE_BYTES) {
645
+ throw new Error('source credential bundle v2 must be bounded canonical JSON')
646
+ }
647
+ let decoded: unknown
648
+ try {
649
+ decoded = JSON.parse(value)
650
+ } catch {
651
+ throw new Error('source credential bundle v2 must be bounded canonical JSON')
652
+ }
653
+ const bundle = sourceCredentialBundleValueV2(decoded)
654
+ if (serializeZeropsSourceCredentialBundleV2(bundle) !== value) {
655
+ throw new Error('source credential bundle v2 must be bounded canonical JSON')
656
+ }
657
+ return bundle
658
+ }
659
+
660
+ export async function sha256ZeropsSourceCredentialBundleV2(value: string): Promise<string> {
661
+ decodeZeropsSourceCredentialBundleV2(value)
662
+ const digest = new Uint8Array(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(value)))
663
+ return [...digest].map((byte) => byte.toString(16).padStart(2, '0')).join('')
664
+ }
665
+
666
+ export async function zeropsSourceCredentialEnvV2(connectionId: string): Promise<string> {
667
+ const canonicalConnectionId = identifier(connectionId, 'source credential connectionId')
668
+ const digest = new Uint8Array(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(canonicalConnectionId)))
669
+ return `${ZEROPS_SOURCE_CREDENTIAL_ENV_V2_PREFIX}${[...digest].map((byte) => byte.toString(16).padStart(2, '0')).join('')}`
670
+ }
671
+
481
672
  const installationId = (value: unknown, label: string): number => {
482
673
  if (typeof value !== 'number' || !Number.isSafeInteger(value) || value <= 0) throw new Error(`${label} must be a positive safe integer`)
483
674
  return value
@@ -487,6 +678,18 @@ const optionalCommitSha = (value: unknown, label: string): string | undefined =>
487
678
 
488
679
  const optionalInstallationId = (value: unknown, label: string): number | undefined => value === undefined ? undefined : installationId(value, label)
489
680
 
681
+ const privateBindingV2 = (value: unknown, label: string): ZeropsSourcePrivateBindingV2 => {
682
+ const parsed = record(value, label)
683
+ exactKeys(parsed, ['connectionId', 'installationId'], label)
684
+ return {
685
+ connectionId: identifier(parsed['connectionId'], `${label}.connectionId`),
686
+ installationId: installationId(parsed['installationId'], `${label}.installationId`),
687
+ }
688
+ }
689
+
690
+ const optionalPrivateBindingV2 = (value: unknown, label: string): ZeropsSourcePrivateBindingV2 | undefined =>
691
+ value === undefined ? undefined : privateBindingV2(value, label)
692
+
490
693
  const repository = (value: unknown, label: string): ZeropsSourceRepository => {
491
694
  const parsed = record(value, label)
492
695
  exactKeys(parsed, ['owner', 'name'], label)
@@ -716,6 +919,117 @@ export function buildZeropsSourceCredentialStatusResponse(
716
919
  return decodeZeropsSourceCredentialStatusResponse({ protocolVersion: ZEROPS_SOURCE_PROTOCOL_VERSION, ...input })
717
920
  }
718
921
 
922
+ export function decodeZeropsSourceCredentialActivateRequestV2(value: unknown): ZeropsSourceCredentialActivateRequestV2 {
923
+ const parsed = record(value, 'source credential activate request v2')
924
+ exactKeys(
925
+ parsed,
926
+ ['protocolVersion', 'connectionId', 'credentialBundle', 'credentialSha256'],
927
+ 'source credential activate request v2',
928
+ )
929
+ const connectionId = identifier(parsed['connectionId'], 'source credential activate request v2.connectionId')
930
+ const credentialBundle = boundedString(
931
+ parsed['credentialBundle'],
932
+ 'source credential activate request v2.credentialBundle',
933
+ MAX_SOURCE_CREDENTIAL_BUNDLE_BYTES,
934
+ )
935
+ const bundle = decodeZeropsSourceCredentialBundleV2(credentialBundle)
936
+ if (bundle.connectionId !== connectionId) throw new Error('source credential activate request v2 connectionId does not match its bundle')
937
+ return {
938
+ protocolVersion: protocolVersionV2(parsed, 'source credential activate request v2'),
939
+ connectionId,
940
+ credentialBundle,
941
+ credentialSha256: sha256(parsed['credentialSha256'], 'source credential activate request v2.credentialSha256'),
942
+ }
943
+ }
944
+
945
+ export function buildZeropsSourceCredentialActivateRequestV2(
946
+ input: ZeropsSourceCredentialActivateInput,
947
+ ): ZeropsSourceCredentialActivateRequestV2 {
948
+ return decodeZeropsSourceCredentialActivateRequestV2({
949
+ protocolVersion: ZEROPS_SOURCE_PROTOCOL_VERSION_V2,
950
+ connectionId: input.connectionId,
951
+ credentialBundle: input.credentialBundle,
952
+ credentialSha256: input.credentialSha256,
953
+ })
954
+ }
955
+
956
+ export function decodeZeropsSourceCredentialActivateResponseV2(value: unknown): ZeropsSourceCredentialActivateResponseV2 {
957
+ const parsed = record(value, 'source credential activate response v2')
958
+ exactKeys(
959
+ parsed,
960
+ ['protocolVersion', 'connectionId', 'credentialVersion', 'credentialSha256', 'githubApp'],
961
+ 'source credential activate response v2',
962
+ )
963
+ if (parsed['credentialVersion'] !== ZEROPS_SOURCE_CREDENTIAL_BUNDLE_VERSION_V2) {
964
+ throw new Error('source credential activate response v2 has an unsupported credentialVersion')
965
+ }
966
+ return {
967
+ protocolVersion: protocolVersionV2(parsed, 'source credential activate response v2'),
968
+ connectionId: identifier(parsed['connectionId'], 'source credential activate response v2.connectionId'),
969
+ credentialVersion: ZEROPS_SOURCE_CREDENTIAL_BUNDLE_VERSION_V2,
970
+ credentialSha256: sha256(parsed['credentialSha256'], 'source credential activate response v2.credentialSha256'),
971
+ githubApp: githubAppIdentity(parsed['githubApp'], 'source credential activate response v2.githubApp'),
972
+ }
973
+ }
974
+
975
+ export function buildZeropsSourceCredentialActivateResponseV2(
976
+ input: Omit<ZeropsSourceCredentialActivateResponseV2, 'protocolVersion'>,
977
+ ): ZeropsSourceCredentialActivateResponseV2 {
978
+ return decodeZeropsSourceCredentialActivateResponseV2({ protocolVersion: ZEROPS_SOURCE_PROTOCOL_VERSION_V2, ...input })
979
+ }
980
+
981
+ export function decodeZeropsSourceCredentialStatusRequestV2(value: unknown): ZeropsSourceCredentialStatusRequestV2 {
982
+ const parsed = record(value, 'source credential status request v2')
983
+ exactKeys(parsed, ['protocolVersion', 'connectionId'], 'source credential status request v2')
984
+ return {
985
+ protocolVersion: protocolVersionV2(parsed, 'source credential status request v2'),
986
+ connectionId: identifier(parsed['connectionId'], 'source credential status request v2.connectionId'),
987
+ }
988
+ }
989
+
990
+ export function buildZeropsSourceCredentialStatusRequestV2(
991
+ input: ZeropsSourceCredentialStatusInput,
992
+ ): ZeropsSourceCredentialStatusRequestV2 {
993
+ return decodeZeropsSourceCredentialStatusRequestV2({
994
+ protocolVersion: ZEROPS_SOURCE_PROTOCOL_VERSION_V2,
995
+ connectionId: input.connectionId,
996
+ })
997
+ }
998
+
999
+ export function decodeZeropsSourceCredentialStatusResponseV2(value: unknown): ZeropsSourceCredentialStatusResponseV2 {
1000
+ const parsed = record(value, 'source credential status response v2')
1001
+ if (parsed['state'] === 'anonymous') {
1002
+ exactKeys(parsed, ['protocolVersion', 'connectionId', 'state'], 'source credential status response v2')
1003
+ return {
1004
+ protocolVersion: protocolVersionV2(parsed, 'source credential status response v2'),
1005
+ connectionId: identifier(parsed['connectionId'], 'source credential status response v2.connectionId'),
1006
+ state: 'anonymous',
1007
+ }
1008
+ }
1009
+ if (parsed['state'] !== 'active') throw new Error('source credential status response v2.state is invalid')
1010
+ exactKeys(
1011
+ parsed,
1012
+ ['protocolVersion', 'connectionId', 'state', 'credentialVersion', 'credentialSha256', 'githubApp'],
1013
+ 'source credential status response v2',
1014
+ )
1015
+ const active = decodeZeropsSourceCredentialActivateResponseV2({
1016
+ protocolVersion: parsed['protocolVersion'],
1017
+ connectionId: parsed['connectionId'],
1018
+ credentialVersion: parsed['credentialVersion'],
1019
+ credentialSha256: parsed['credentialSha256'],
1020
+ githubApp: parsed['githubApp'],
1021
+ })
1022
+ return { ...active, state: 'active' }
1023
+ }
1024
+
1025
+ export function buildZeropsSourceCredentialStatusResponseV2(
1026
+ input:
1027
+ | { readonly connectionId: string; readonly state: 'anonymous' }
1028
+ | Omit<ZeropsSourceCredentialActiveStatusResponseV2, 'protocolVersion'>,
1029
+ ): ZeropsSourceCredentialStatusResponseV2 {
1030
+ return decodeZeropsSourceCredentialStatusResponseV2({ protocolVersion: ZEROPS_SOURCE_PROTOCOL_VERSION_V2, ...input })
1031
+ }
1032
+
719
1033
  const webhookUrl = (value: unknown, label: string): string => {
720
1034
  const candidate = boundedString(value, label, MAX_WEBHOOK_URL_LENGTH)
721
1035
  let parsed: URL
@@ -988,6 +1302,104 @@ export function buildZeropsSourceUploadResponse(result: ZeropsSourceUploadResult
988
1302
  return decodeZeropsSourceUploadResponse({ protocolVersion: ZEROPS_SOURCE_PROTOCOL_VERSION, ...result })
989
1303
  }
990
1304
 
1305
+ export function decodeZeropsSourceResolveRequestV2(value: unknown): ZeropsSourceResolveRequestV2 {
1306
+ const parsed = record(value, 'source resolve request v2')
1307
+ optionalFields(
1308
+ parsed,
1309
+ ['protocolVersion', 'runId', 'repository', 'requestedRef', 'descriptorSha256'],
1310
+ ['expectedCommitSha', 'privateBinding'],
1311
+ 'source resolve request v2',
1312
+ )
1313
+ const expected = optionalCommitSha(parsed['expectedCommitSha'], 'source resolve request v2.expectedCommitSha')
1314
+ const binding = optionalPrivateBindingV2(parsed['privateBinding'], 'source resolve request v2.privateBinding')
1315
+ return {
1316
+ protocolVersion: protocolVersionV2(parsed, 'source resolve request v2'),
1317
+ runId: identifier(parsed['runId'], 'source resolve request v2.runId'),
1318
+ repository: repository(parsed['repository'], 'source resolve request v2.repository'),
1319
+ requestedRef: ref(parsed['requestedRef'], 'source resolve request v2.requestedRef'),
1320
+ ...(expected !== undefined ? { expectedCommitSha: expected } : {}),
1321
+ ...(binding !== undefined ? { privateBinding: binding } : {}),
1322
+ descriptorSha256: sha256(parsed['descriptorSha256'], 'source resolve request v2.descriptorSha256'),
1323
+ }
1324
+ }
1325
+
1326
+ export function buildZeropsSourceResolveRequestV2(input: ZeropsSourceResolveInputV2): ZeropsSourceResolveRequestV2 {
1327
+ return decodeZeropsSourceResolveRequestV2({
1328
+ protocolVersion: ZEROPS_SOURCE_PROTOCOL_VERSION_V2,
1329
+ runId: input.runId,
1330
+ repository: input.repository,
1331
+ requestedRef: input.requestedRef,
1332
+ ...(input.expectedCommitSha !== undefined ? { expectedCommitSha: input.expectedCommitSha } : {}),
1333
+ ...(input.privateBinding !== undefined ? { privateBinding: input.privateBinding } : {}),
1334
+ descriptorSha256: input.descriptorSha256,
1335
+ })
1336
+ }
1337
+
1338
+ export function decodeZeropsSourceResolveResponseV2(value: unknown): ZeropsSourceResolveResponseV2 {
1339
+ const parsed = record(value, 'source resolve response v2')
1340
+ exactKeys(parsed, ['protocolVersion', 'runId', 'commitSha', 'descriptorSha256'], 'source resolve response v2')
1341
+ return {
1342
+ protocolVersion: protocolVersionV2(parsed, 'source resolve response v2'),
1343
+ runId: identifier(parsed['runId'], 'source resolve response v2.runId'),
1344
+ commitSha: commitSha(parsed['commitSha'], 'source resolve response v2.commitSha'),
1345
+ descriptorSha256: sha256(parsed['descriptorSha256'], 'source resolve response v2.descriptorSha256'),
1346
+ }
1347
+ }
1348
+
1349
+ export function buildZeropsSourceResolveResponseV2(result: ZeropsSourceResolveResult): ZeropsSourceResolveResponseV2 {
1350
+ return decodeZeropsSourceResolveResponseV2({ protocolVersion: ZEROPS_SOURCE_PROTOCOL_VERSION_V2, ...result })
1351
+ }
1352
+
1353
+ export function decodeZeropsSourceUploadRequestV2(value: unknown): ZeropsSourceUploadRequestV2 {
1354
+ const parsed = record(value, 'source upload request v2')
1355
+ optionalFields(
1356
+ parsed,
1357
+ ['protocolVersion', 'runId', 'appVersionId', 'repository', 'commitSha', 'uploadUrl', 'descriptor'],
1358
+ ['privateBinding'],
1359
+ 'source upload request v2',
1360
+ )
1361
+ const binding = optionalPrivateBindingV2(parsed['privateBinding'], 'source upload request v2.privateBinding')
1362
+ return {
1363
+ protocolVersion: protocolVersionV2(parsed, 'source upload request v2'),
1364
+ runId: identifier(parsed['runId'], 'source upload request v2.runId'),
1365
+ appVersionId: identifier(parsed['appVersionId'], 'source upload request v2.appVersionId'),
1366
+ repository: repository(parsed['repository'], 'source upload request v2.repository'),
1367
+ commitSha: commitSha(parsed['commitSha'], 'source upload request v2.commitSha'),
1368
+ ...(binding !== undefined ? { privateBinding: binding } : {}),
1369
+ uploadUrl: uploadUrl(parsed['uploadUrl'], 'source upload request v2.uploadUrl'),
1370
+ descriptor: descriptor(parsed['descriptor'], 'source upload request v2.descriptor'),
1371
+ }
1372
+ }
1373
+
1374
+ export function buildZeropsSourceUploadRequestV2(input: ZeropsSourceUploadInputV2): ZeropsSourceUploadRequestV2 {
1375
+ return decodeZeropsSourceUploadRequestV2({
1376
+ protocolVersion: ZEROPS_SOURCE_PROTOCOL_VERSION_V2,
1377
+ runId: input.runId,
1378
+ appVersionId: input.appVersionId,
1379
+ repository: input.repository,
1380
+ commitSha: input.commitSha,
1381
+ ...(input.privateBinding !== undefined ? { privateBinding: input.privateBinding } : {}),
1382
+ uploadUrl: input.uploadUrl,
1383
+ descriptor: input.descriptor,
1384
+ })
1385
+ }
1386
+
1387
+ export function decodeZeropsSourceUploadResponseV2(value: unknown): ZeropsSourceUploadResponseV2 {
1388
+ const parsed = record(value, 'source upload response v2')
1389
+ exactKeys(parsed, ['protocolVersion', 'runId', 'appVersionId', 'commitSha', 'descriptorSha256'], 'source upload response v2')
1390
+ return {
1391
+ protocolVersion: protocolVersionV2(parsed, 'source upload response v2'),
1392
+ runId: identifier(parsed['runId'], 'source upload response v2.runId'),
1393
+ appVersionId: identifier(parsed['appVersionId'], 'source upload response v2.appVersionId'),
1394
+ commitSha: commitSha(parsed['commitSha'], 'source upload response v2.commitSha'),
1395
+ descriptorSha256: sha256(parsed['descriptorSha256'], 'source upload response v2.descriptorSha256'),
1396
+ }
1397
+ }
1398
+
1399
+ export function buildZeropsSourceUploadResponseV2(result: ZeropsSourceUploadResult): ZeropsSourceUploadResponseV2 {
1400
+ return decodeZeropsSourceUploadResponseV2({ protocolVersion: ZEROPS_SOURCE_PROTOCOL_VERSION_V2, ...result })
1401
+ }
1402
+
991
1403
  export function decodeZeropsSourceCancelRequest(value: unknown): ZeropsSourceCancelRequestV1 {
992
1404
  const parsed = record(value, 'source cancel request')
993
1405
  exactKeys(parsed, ['protocolVersion', 'runId', 'appVersionId'], 'source cancel request')