@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/src/source.ts ADDED
@@ -0,0 +1,1043 @@
1
+ export const ZEROPS_SOURCE_PROTOCOL_VERSION = 1
2
+ export const ZEROPS_SOURCE_RESOLVE_INSTALLATION_PATH = '/v1/installations/resolve'
3
+ export const ZEROPS_SOURCE_RESOLVE_PATH = '/v1/source/resolve'
4
+ export const ZEROPS_SOURCE_UPLOAD_PATH = '/v1/source/upload'
5
+ export const ZEROPS_SOURCE_CANCEL_PATH = '/v1/source/cancel'
6
+ export const ZEROPS_SOURCE_CREDENTIAL_ACTIVATE_PATH = '/v1/source/credentials/activate'
7
+ export const ZEROPS_SOURCE_CREDENTIAL_STATUS_PATH = '/v1/source/credentials/status'
8
+ export const ZEROPS_SOURCE_WEBHOOK_CONFIGURE_PATH = '/v1/source/github/webhook/configure'
9
+ export const ZEROPS_SOURCE_INSTALLATIONS_VERIFY_PATH = '/v1/source/github/installations/verify'
10
+ export const ZEROPS_SOURCE_CREDENTIAL_BUNDLE_VERSION = 1
11
+
12
+ const MAX_ID_LENGTH = 128
13
+ const MAX_REF_LENGTH = 255
14
+ const MAX_UPLOAD_URL_LENGTH = 4096
15
+ const MAX_SOURCE_CREDENTIAL_BUNDLE_BYTES = 72 * 1024
16
+ const MAX_GITHUB_APP_PRIVATE_KEY_LENGTH = 64 * 1024
17
+ const MAX_WEBHOOK_URL_LENGTH = 2048
18
+ const MAX_WEBHOOK_SECRET_LENGTH = 4096
19
+ const MAX_INSTALLATION_REPOSITORIES = 100
20
+ const SHA256_PATTERN = /^[a-f0-9]{64}$/
21
+ const COMMIT_SHA_PATTERN = /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/
22
+ const ID_PATTERN = /^[A-Za-z0-9._:-]+$/
23
+ const GITHUB_OWNER_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,37}[a-z0-9])?$/
24
+ const GITHUB_REPOSITORY_PATTERN = /^[a-z0-9._-]{1,100}$/
25
+ const GITHUB_APP_ID_PATTERN = /^[1-9][0-9]{0,31}$/
26
+ const GITHUB_APP_SLUG_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,98}[a-z0-9])?$/
27
+ const GITHUB_LOGIN_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,98}[A-Za-z0-9])?$/
28
+
29
+ export interface ZeropsSourceCredentialBundleV1 {
30
+ readonly version: typeof ZEROPS_SOURCE_CREDENTIAL_BUNDLE_VERSION
31
+ readonly githubAppId: string
32
+ readonly privateKeyPem: string
33
+ }
34
+
35
+ export interface ZeropsSourceGitHubAppIdentityV1 {
36
+ readonly id: number
37
+ readonly slug: string
38
+ readonly htmlUrl: string
39
+ readonly public: boolean
40
+ readonly owner: {
41
+ readonly login: string
42
+ readonly type: 'Organization'
43
+ }
44
+ readonly permissions: {
45
+ readonly contents: 'read'
46
+ }
47
+ readonly events: readonly ['push']
48
+ }
49
+
50
+ export interface ZeropsSourceCredentialActivateRequestV1 {
51
+ readonly protocolVersion: typeof ZEROPS_SOURCE_PROTOCOL_VERSION
52
+ readonly connectionId: string
53
+ /** Canonical JSON that is byte-identical to the durable source environment value. */
54
+ readonly credentialBundle: string
55
+ readonly credentialSha256: string
56
+ }
57
+
58
+ export interface ZeropsSourceCredentialActivateResponseV1 {
59
+ readonly protocolVersion: typeof ZEROPS_SOURCE_PROTOCOL_VERSION
60
+ readonly connectionId: string
61
+ readonly credentialVersion: typeof ZEROPS_SOURCE_CREDENTIAL_BUNDLE_VERSION
62
+ readonly credentialSha256: string
63
+ readonly githubApp: ZeropsSourceGitHubAppIdentityV1
64
+ }
65
+
66
+ export interface ZeropsSourceCredentialStatusRequestV1 {
67
+ readonly protocolVersion: typeof ZEROPS_SOURCE_PROTOCOL_VERSION
68
+ readonly connectionId: string
69
+ }
70
+
71
+ export interface ZeropsSourceCredentialAnonymousStatusResponseV1 {
72
+ readonly protocolVersion: typeof ZEROPS_SOURCE_PROTOCOL_VERSION
73
+ readonly connectionId: string
74
+ readonly state: 'anonymous'
75
+ }
76
+
77
+ export interface ZeropsSourceCredentialActiveStatusResponseV1 extends ZeropsSourceCredentialActivateResponseV1 {
78
+ readonly state: 'active'
79
+ }
80
+
81
+ export type ZeropsSourceCredentialStatusResponseV1 =
82
+ | ZeropsSourceCredentialAnonymousStatusResponseV1
83
+ | ZeropsSourceCredentialActiveStatusResponseV1
84
+
85
+ export interface ZeropsSourceWebhookConfigureInput {
86
+ readonly connectionId: string
87
+ readonly credentialSha256: string
88
+ readonly url: string
89
+ readonly secret: string
90
+ readonly signal: AbortSignal
91
+ }
92
+
93
+ export interface ZeropsSourceWebhookConfigureRequestV1 {
94
+ readonly protocolVersion: typeof ZEROPS_SOURCE_PROTOCOL_VERSION
95
+ readonly connectionId: string
96
+ readonly credentialSha256: string
97
+ readonly url: string
98
+ readonly secret: string
99
+ }
100
+
101
+ export interface ZeropsSourceWebhookConfigureResponseV1 {
102
+ readonly protocolVersion: typeof ZEROPS_SOURCE_PROTOCOL_VERSION
103
+ readonly connectionId: string
104
+ readonly credentialSha256: string
105
+ readonly webhook: { readonly url: string; readonly contentType: 'json'; readonly insecureSsl: '0' }
106
+ }
107
+
108
+ export type ZeropsSourceInstallationScopeV1 =
109
+ | { readonly kind: 'organization'; readonly organization: string }
110
+ | { readonly kind: 'repositories'; readonly repositories: readonly ZeropsSourceRepository[] }
111
+
112
+ export interface ZeropsSourceInstallationsVerifyInput {
113
+ readonly connectionId: string
114
+ readonly credentialSha256: string
115
+ readonly scope: ZeropsSourceInstallationScopeV1
116
+ readonly signal: AbortSignal
117
+ }
118
+
119
+ export interface ZeropsSourceInstallationsVerifyRequestV1 {
120
+ readonly protocolVersion: typeof ZEROPS_SOURCE_PROTOCOL_VERSION
121
+ readonly connectionId: string
122
+ readonly credentialSha256: string
123
+ readonly scope: ZeropsSourceInstallationScopeV1
124
+ }
125
+
126
+ export type ZeropsSourceInstallationVerificationV1 =
127
+ | { readonly status: 'missing' }
128
+ | {
129
+ readonly status: 'installed'
130
+ readonly installationId: number
131
+ readonly accountLogin: string
132
+ readonly repositorySelection: 'all' | 'selected'
133
+ }
134
+
135
+ export interface ZeropsSourceInstallationsVerifyResponseV1 {
136
+ readonly protocolVersion: typeof ZEROPS_SOURCE_PROTOCOL_VERSION
137
+ readonly connectionId: string
138
+ readonly credentialSha256: string
139
+ readonly installation: ZeropsSourceInstallationVerificationV1
140
+ }
141
+
142
+ export interface ZeropsSourceRepository {
143
+ owner: string
144
+ name: string
145
+ }
146
+
147
+ export interface ZeropsSourceDescriptor {
148
+ path: 'zerops.yaml'
149
+ sha256: string
150
+ }
151
+
152
+ export interface ZeropsSourceResolveInstallationRequestV1 {
153
+ protocolVersion: typeof ZEROPS_SOURCE_PROTOCOL_VERSION
154
+ repository: ZeropsSourceRepository
155
+ }
156
+
157
+ export interface ZeropsSourceResolveInstallationResponseV1 {
158
+ protocolVersion: typeof ZEROPS_SOURCE_PROTOCOL_VERSION
159
+ githubInstallationId: number | null
160
+ }
161
+
162
+ export interface ZeropsSourceResolveRequestV1 {
163
+ protocolVersion: typeof ZEROPS_SOURCE_PROTOCOL_VERSION
164
+ runId: string
165
+ repository: ZeropsSourceRepository
166
+ requestedRef: string
167
+ expectedCommitSha?: string
168
+ githubInstallationId?: number
169
+ descriptorSha256: string
170
+ }
171
+
172
+ export interface ZeropsSourceResolveResponseV1 {
173
+ protocolVersion: typeof ZEROPS_SOURCE_PROTOCOL_VERSION
174
+ runId: string
175
+ commitSha: string
176
+ descriptorSha256: string
177
+ }
178
+
179
+ export interface ZeropsSourceUploadRequestV1 {
180
+ protocolVersion: typeof ZEROPS_SOURCE_PROTOCOL_VERSION
181
+ runId: string
182
+ appVersionId: string
183
+ repository: ZeropsSourceRepository
184
+ commitSha: string
185
+ githubInstallationId?: number
186
+ uploadUrl: string
187
+ descriptor: ZeropsSourceDescriptor
188
+ }
189
+
190
+ export interface ZeropsSourceUploadResponseV1 {
191
+ protocolVersion: typeof ZEROPS_SOURCE_PROTOCOL_VERSION
192
+ runId: string
193
+ appVersionId: string
194
+ commitSha: string
195
+ descriptorSha256: string
196
+ }
197
+
198
+ export interface ZeropsSourceCancelRequestV1 {
199
+ protocolVersion: typeof ZEROPS_SOURCE_PROTOCOL_VERSION
200
+ runId: string
201
+ appVersionId: string
202
+ }
203
+
204
+ export interface ZeropsSourceCancelResponseV1 {
205
+ protocolVersion: typeof ZEROPS_SOURCE_PROTOCOL_VERSION
206
+ runId: string
207
+ appVersionId: string
208
+ }
209
+
210
+ export interface ZeropsSourceCancelResult {
211
+ runId: string
212
+ appVersionId: string
213
+ }
214
+
215
+ export type ZeropsSourceErrorCode =
216
+ | 'invalid_request'
217
+ | 'unauthorized'
218
+ | 'repository_not_found'
219
+ | 'installation_not_found'
220
+ | 'ref_not_found'
221
+ | 'commit_mismatch'
222
+ | 'descriptor_missing'
223
+ | 'descriptor_mismatch'
224
+ | 'archive_rejected'
225
+ | 'upload_rejected'
226
+ | 'upload_failed'
227
+ | 'credentials_invalid'
228
+ | 'credentials_conflict'
229
+ | 'cancelled'
230
+ | 'internal'
231
+
232
+ export const ZEROPS_SOURCE_ERROR_CODES: readonly ZeropsSourceErrorCode[] = [
233
+ 'invalid_request',
234
+ 'unauthorized',
235
+ 'repository_not_found',
236
+ 'installation_not_found',
237
+ 'ref_not_found',
238
+ 'commit_mismatch',
239
+ 'descriptor_missing',
240
+ 'descriptor_mismatch',
241
+ 'archive_rejected',
242
+ 'upload_rejected',
243
+ 'upload_failed',
244
+ 'credentials_invalid',
245
+ 'credentials_conflict',
246
+ 'cancelled',
247
+ 'internal',
248
+ ]
249
+
250
+ export type ZeropsSourceErrorStage =
251
+ | 'authenticate'
252
+ | 'validate'
253
+ | 'resolve-installation'
254
+ | 'resolve'
255
+ | 'archive'
256
+ | 'upload'
257
+ | 'cancel'
258
+ | 'credentials'
259
+
260
+ export const ZEROPS_SOURCE_ERROR_STAGES: readonly ZeropsSourceErrorStage[] = [
261
+ 'authenticate',
262
+ 'validate',
263
+ 'resolve-installation',
264
+ 'resolve',
265
+ 'archive',
266
+ 'upload',
267
+ 'cancel',
268
+ 'credentials',
269
+ ]
270
+
271
+ export interface ZeropsSourceErrorEnvelope {
272
+ error: {
273
+ code: ZeropsSourceErrorCode
274
+ stage: ZeropsSourceErrorStage
275
+ retryable: boolean
276
+ }
277
+ }
278
+
279
+ export interface ZeropsSourceResolveInput {
280
+ runId: string
281
+ repository: ZeropsSourceRepository
282
+ requestedRef: string
283
+ expectedCommitSha?: string
284
+ githubInstallationId?: number
285
+ descriptorSha256: string
286
+ signal: AbortSignal
287
+ }
288
+
289
+ export interface ZeropsSourceResolveResult {
290
+ runId: string
291
+ commitSha: string
292
+ descriptorSha256: string
293
+ }
294
+
295
+ export interface ZeropsSourceUploadInput {
296
+ runId: string
297
+ appVersionId: string
298
+ repository: ZeropsSourceRepository
299
+ commitSha: string
300
+ githubInstallationId?: number
301
+ uploadUrl: string
302
+ descriptor: ZeropsSourceDescriptor
303
+ signal: AbortSignal
304
+ }
305
+
306
+ export interface ZeropsSourceUploadResult {
307
+ runId: string
308
+ appVersionId: string
309
+ commitSha: string
310
+ descriptorSha256: string
311
+ }
312
+
313
+ export interface ZeropsSourceCancelInput {
314
+ runId: string
315
+ appVersionId: string
316
+ signal: AbortSignal
317
+ }
318
+
319
+ export interface ZeropsSourceClient {
320
+ resolveInstallationId(repoUrl: string, signal: AbortSignal): Promise<number | null>
321
+ resolve(input: ZeropsSourceResolveInput): Promise<ZeropsSourceResolveResult>
322
+ upload(input: ZeropsSourceUploadInput): Promise<ZeropsSourceUploadResult>
323
+ cancel(input: ZeropsSourceCancelInput): Promise<void>
324
+ }
325
+
326
+ export interface ZeropsSourceCredentialActivateInput {
327
+ readonly connectionId: string
328
+ readonly credentialBundle: string
329
+ readonly credentialSha256: string
330
+ readonly signal: AbortSignal
331
+ }
332
+
333
+ export interface ZeropsSourceCredentialStatusInput {
334
+ readonly connectionId: string
335
+ readonly signal: AbortSignal
336
+ }
337
+
338
+ /** Credential management is separate from the deploy source client and is composed only in the authenticated admin flow. */
339
+ export interface ZeropsSourceCredentialManager {
340
+ activate(input: ZeropsSourceCredentialActivateInput): Promise<ZeropsSourceCredentialActivateResponseV1>
341
+ status(input: ZeropsSourceCredentialStatusInput): Promise<ZeropsSourceCredentialStatusResponseV1>
342
+ configureWebhook(input: ZeropsSourceWebhookConfigureInput): Promise<ZeropsSourceWebhookConfigureResponseV1>
343
+ verifyInstallations(input: ZeropsSourceInstallationsVerifyInput): Promise<ZeropsSourceInstallationsVerifyResponseV1>
344
+ }
345
+
346
+ const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === 'object' && value !== null && !Array.isArray(value)
347
+
348
+ const record = (value: unknown, label: string): Record<string, unknown> => {
349
+ if (!isRecord(value)) throw new Error(`${label} must be an object`)
350
+ return value
351
+ }
352
+
353
+ const exactKeys = (value: Record<string, unknown>, allowed: readonly string[], label: string): void => {
354
+ const allowedKeys = new Set(allowed)
355
+ const unknownKey = Object.keys(value).find((key) => !allowedKeys.has(key))
356
+ if (unknownKey !== undefined) throw new Error(`${label} contains an unknown field`)
357
+ const missingKey = allowed.find((key) => !(key in value))
358
+ if (missingKey !== undefined) throw new Error(`${label} is missing field ${missingKey}`)
359
+ }
360
+
361
+ const protocolVersion = (value: Record<string, unknown>, label: string): typeof ZEROPS_SOURCE_PROTOCOL_VERSION => {
362
+ if (value['protocolVersion'] !== ZEROPS_SOURCE_PROTOCOL_VERSION) throw new Error(`${label} has an unsupported protocolVersion`)
363
+ return ZEROPS_SOURCE_PROTOCOL_VERSION
364
+ }
365
+
366
+ const boundedString = (value: unknown, label: string, maximum: number): string => {
367
+ if (typeof value !== 'string' || value.length === 0 || value.length > maximum) {
368
+ throw new Error(`${label} must be a non-empty string of at most ${maximum} characters`)
369
+ }
370
+ return value
371
+ }
372
+
373
+ const identifier = (value: unknown, label: string): string => {
374
+ const parsed = boundedString(value, label, MAX_ID_LENGTH)
375
+ if (!ID_PATTERN.test(parsed)) throw new Error(`${label} contains invalid characters`)
376
+ return parsed
377
+ }
378
+
379
+ const ref = (value: unknown, label: string): string => {
380
+ const parsed = boundedString(value, label, MAX_REF_LENGTH)
381
+ if (COMMIT_SHA_PATTERN.test(parsed)) return parsed
382
+ const hasForbiddenCharacter = [...parsed].some((character) => {
383
+ const code = character.charCodeAt(0)
384
+ return code <= 0x20 || code === 0x7f || '~^:?*[\\'.includes(character)
385
+ })
386
+ const components = parsed.split('/')
387
+ if (
388
+ parsed.startsWith('-')
389
+ || parsed.trim() !== parsed
390
+ || parsed === '@'
391
+ || parsed.includes('..')
392
+ || parsed.includes('@{')
393
+ || parsed.includes('//')
394
+ || parsed.endsWith('/')
395
+ || parsed.endsWith('.')
396
+ || hasForbiddenCharacter
397
+ || components.some((component) => component === '' || component.startsWith('.') || component.endsWith('.lock'))
398
+ ) {
399
+ throw new Error(`${label} must be a safe Git ref or lowercase commit digest`)
400
+ }
401
+ return parsed
402
+ }
403
+
404
+ const commitSha = (value: unknown, label: string): string => {
405
+ if (typeof value !== 'string' || !COMMIT_SHA_PATTERN.test(value)) throw new Error(`${label} must be a lowercase 40- or 64-character hex digest`)
406
+ return value
407
+ }
408
+
409
+ const sha256 = (value: unknown, label: string): string => {
410
+ if (typeof value !== 'string' || !SHA256_PATTERN.test(value)) throw new Error(`${label} must be a lowercase 64-character hex digest`)
411
+ return value
412
+ }
413
+
414
+ const sourceCredentialBundleValue = (value: unknown): ZeropsSourceCredentialBundleV1 => {
415
+ const parsed = record(value, 'source credential bundle')
416
+ exactKeys(parsed, ['version', 'githubAppId', 'privateKeyPem'], 'source credential bundle')
417
+ if (parsed['version'] !== ZEROPS_SOURCE_CREDENTIAL_BUNDLE_VERSION) {
418
+ throw new Error('source credential bundle has an unsupported version')
419
+ }
420
+ const githubAppId = boundedString(parsed['githubAppId'], 'source credential bundle.githubAppId', 32)
421
+ if (!GITHUB_APP_ID_PATTERN.test(githubAppId)) throw new Error('source credential bundle.githubAppId is invalid')
422
+ const privateKeyPem = boundedString(
423
+ parsed['privateKeyPem'],
424
+ 'source credential bundle.privateKeyPem',
425
+ MAX_GITHUB_APP_PRIVATE_KEY_LENGTH,
426
+ )
427
+ if (!isCanonicalPrivateKeyPem(privateKeyPem)) throw new Error('source credential bundle.privateKeyPem is invalid')
428
+ return { version: ZEROPS_SOURCE_CREDENTIAL_BUNDLE_VERSION, githubAppId, privateKeyPem }
429
+ }
430
+
431
+ const isCanonicalPrivateKeyPem = (value: string): boolean => {
432
+ 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
+ if (match === null || match[1] !== match[3]) return false
434
+ const body = match[2]
435
+ if (body === undefined) return false
436
+ const lines = body.split('\n')
437
+ if (lines.some((line) => line.length === 0 || line.length > 64)) return false
438
+ if (lines.slice(0, -1).some((line) => line.includes('='))) return false
439
+ const encoded = lines.join('')
440
+ if (encoded.length % 4 !== 0 || !/^[A-Za-z0-9+/]+={0,2}$/.test(encoded)) return false
441
+ try {
442
+ const decoded = atob(encoded)
443
+ return decoded.length > 0 && decoded.charCodeAt(0) === 0x30
444
+ } catch {
445
+ return false
446
+ }
447
+ }
448
+
449
+ export function buildZeropsSourceCredentialBundle(
450
+ input: Omit<ZeropsSourceCredentialBundleV1, 'version'>,
451
+ ): ZeropsSourceCredentialBundleV1 {
452
+ return sourceCredentialBundleValue({ version: ZEROPS_SOURCE_CREDENTIAL_BUNDLE_VERSION, ...input })
453
+ }
454
+
455
+ export function serializeZeropsSourceCredentialBundle(bundle: ZeropsSourceCredentialBundleV1): string {
456
+ const parsed = sourceCredentialBundleValue(bundle)
457
+ return JSON.stringify({ version: parsed.version, githubAppId: parsed.githubAppId, privateKeyPem: parsed.privateKeyPem })
458
+ }
459
+
460
+ export function decodeZeropsSourceCredentialBundle(value: unknown): ZeropsSourceCredentialBundleV1 {
461
+ if (typeof value !== 'string' || new TextEncoder().encode(value).byteLength > MAX_SOURCE_CREDENTIAL_BUNDLE_BYTES) {
462
+ throw new Error('source credential bundle must be bounded canonical JSON')
463
+ }
464
+ let decoded: unknown
465
+ try {
466
+ decoded = JSON.parse(value)
467
+ } catch {
468
+ throw new Error('source credential bundle must be bounded canonical JSON')
469
+ }
470
+ const bundle = sourceCredentialBundleValue(decoded)
471
+ if (serializeZeropsSourceCredentialBundle(bundle) !== value) throw new Error('source credential bundle must be bounded canonical JSON')
472
+ return bundle
473
+ }
474
+
475
+ export async function sha256ZeropsSourceCredentialBundle(value: string): Promise<string> {
476
+ decodeZeropsSourceCredentialBundle(value)
477
+ const digest = new Uint8Array(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(value)))
478
+ return [...digest].map((byte) => byte.toString(16).padStart(2, '0')).join('')
479
+ }
480
+
481
+ const installationId = (value: unknown, label: string): number => {
482
+ if (typeof value !== 'number' || !Number.isSafeInteger(value) || value <= 0) throw new Error(`${label} must be a positive safe integer`)
483
+ return value
484
+ }
485
+
486
+ const optionalCommitSha = (value: unknown, label: string): string | undefined => value === undefined ? undefined : commitSha(value, label)
487
+
488
+ const optionalInstallationId = (value: unknown, label: string): number | undefined => value === undefined ? undefined : installationId(value, label)
489
+
490
+ const repository = (value: unknown, label: string): ZeropsSourceRepository => {
491
+ const parsed = record(value, label)
492
+ exactKeys(parsed, ['owner', 'name'], label)
493
+ const owner = boundedString(parsed['owner'], `${label}.owner`, 39)
494
+ const name = boundedString(parsed['name'], `${label}.name`, 100)
495
+ if (!GITHUB_OWNER_PATTERN.test(owner)) throw new Error(`${label}.owner must be a normalized lowercase GitHub owner`)
496
+ if (!GITHUB_REPOSITORY_PATTERN.test(name) || name === '.' || name === '..') {
497
+ throw new Error(`${label}.name must be a normalized lowercase GitHub repository name`)
498
+ }
499
+ return { owner, name }
500
+ }
501
+
502
+ const descriptor = (value: unknown, label: string): ZeropsSourceDescriptor => {
503
+ const parsed = record(value, label)
504
+ exactKeys(parsed, ['path', 'sha256'], label)
505
+ if (parsed['path'] !== 'zerops.yaml') throw new Error(`${label}.path must be zerops.yaml`)
506
+ return { path: 'zerops.yaml', sha256: sha256(parsed['sha256'], `${label}.sha256`) }
507
+ }
508
+
509
+ const githubAppIdentity = (value: unknown, label: string): ZeropsSourceGitHubAppIdentityV1 => {
510
+ const parsed = record(value, label)
511
+ exactKeys(parsed, ['id', 'slug', 'htmlUrl', 'public', 'owner', 'permissions', 'events'], label)
512
+ const id = installationId(parsed['id'], `${label}.id`)
513
+ const slug = boundedString(parsed['slug'], `${label}.slug`, 100)
514
+ if (!GITHUB_APP_SLUG_PATTERN.test(slug)) throw new Error(`${label}.slug is invalid`)
515
+ const htmlUrl = boundedString(parsed['htmlUrl'], `${label}.htmlUrl`, 2048)
516
+ let parsedHtmlUrl: URL
517
+ try {
518
+ parsedHtmlUrl = new URL(htmlUrl)
519
+ } catch {
520
+ throw new Error(`${label}.htmlUrl is invalid`)
521
+ }
522
+ if (
523
+ parsedHtmlUrl.protocol !== 'https:' || parsedHtmlUrl.hostname !== 'github.com' || parsedHtmlUrl.port !== ''
524
+ || parsedHtmlUrl.username !== '' || parsedHtmlUrl.password !== '' || parsedHtmlUrl.search !== '' || parsedHtmlUrl.hash !== ''
525
+ || parsedHtmlUrl.pathname !== `/apps/${slug}`
526
+ ) {
527
+ throw new Error(`${label}.htmlUrl is invalid`)
528
+ }
529
+ if (typeof parsed['public'] !== 'boolean') throw new Error(`${label}.public must be boolean`)
530
+ const owner = record(parsed['owner'], `${label}.owner`)
531
+ exactKeys(owner, ['login', 'type'], `${label}.owner`)
532
+ const login = boundedString(owner['login'], `${label}.owner.login`, 100)
533
+ if (!GITHUB_LOGIN_PATTERN.test(login) || owner['type'] !== 'Organization') throw new Error(`${label}.owner is invalid`)
534
+ const permissions = record(parsed['permissions'], `${label}.permissions`)
535
+ exactKeys(permissions, ['contents'], `${label}.permissions`)
536
+ if (permissions['contents'] !== 'read') throw new Error(`${label}.permissions is invalid`)
537
+ if (!Array.isArray(parsed['events']) || parsed['events'].length !== 1 || parsed['events'][0] !== 'push') {
538
+ throw new Error(`${label}.events is invalid`)
539
+ }
540
+ return {
541
+ id,
542
+ slug,
543
+ htmlUrl,
544
+ public: parsed['public'],
545
+ owner: { login, type: 'Organization' },
546
+ permissions: { contents: 'read' },
547
+ events: ['push'],
548
+ }
549
+ }
550
+
551
+ const optionalFields = (value: Record<string, unknown>, required: readonly string[], optional: readonly string[], label: string): void => {
552
+ const allowed = [...required, ...optional]
553
+ const unknownKey = Object.keys(value).find((key) => !allowed.includes(key))
554
+ if (unknownKey !== undefined) throw new Error(`${label} contains an unknown field`)
555
+ const missingKey = required.find((key) => !(key in value))
556
+ if (missingKey !== undefined) throw new Error(`${label} is missing field ${missingKey}`)
557
+ for (const key of optional) {
558
+ if (key in value && value[key] === undefined) throw new Error(`${label}.${key} must be omitted instead of undefined`)
559
+ }
560
+ }
561
+
562
+ /** Validate only credential-safe URL structure; the source runtime owns the exact Zerops destination allowlist. */
563
+ const uploadUrl = (value: unknown, label: string): string => {
564
+ const candidate = boundedString(value, label, MAX_UPLOAD_URL_LENGTH)
565
+ if (/\s/u.test(candidate) || candidate.includes('#')) throw new Error(`${label} must be a signed HTTPS URL`)
566
+ let parsed: URL
567
+ try {
568
+ parsed = new URL(candidate)
569
+ } catch {
570
+ throw new Error(`${label} must be a signed HTTPS URL`)
571
+ }
572
+ if (
573
+ parsed.protocol !== 'https:'
574
+ || parsed.username !== ''
575
+ || parsed.password !== ''
576
+ || parsed.hash !== ''
577
+ || parsed.search === ''
578
+ ) {
579
+ throw new Error(`${label} must be a signed HTTPS URL without userinfo or fragment`)
580
+ }
581
+ return candidate
582
+ }
583
+
584
+ export function normalizeZeropsSourceRepository(repoUrl: string): ZeropsSourceRepository {
585
+ if (/\s/u.test(repoUrl) || /\p{Cc}/u.test(repoUrl)) {
586
+ throw new Error('source repository URL must be a canonical GitHub repository URL')
587
+ }
588
+ const candidate = repoUrl.startsWith('github.com/') ? `https://${repoUrl}` : repoUrl
589
+ let parsed: URL
590
+ try {
591
+ parsed = new URL(candidate)
592
+ } catch {
593
+ throw new Error('source repository URL must be a canonical GitHub repository URL')
594
+ }
595
+ if (
596
+ parsed.protocol !== 'https:' || parsed.hostname !== 'github.com' || parsed.port !== '' || parsed.username !== '' || parsed.password !== ''
597
+ || parsed.search !== '' || parsed.hash !== ''
598
+ ) {
599
+ throw new Error('source repository URL must be an HTTPS GitHub URL without credentials, port, query, or fragment')
600
+ }
601
+ const segments = parsed.pathname.split('/')
602
+ if (segments.length !== 3 || segments[0] !== '') throw new Error('source repository URL must identify one GitHub repository')
603
+ const owner = segments[1]
604
+ const rawName = segments[2]
605
+ if (owner === undefined || rawName === undefined) throw new Error('source repository URL must identify one GitHub repository')
606
+ const name = rawName.endsWith('.git') ? rawName.slice(0, -4) : rawName
607
+ return repository({ owner: owner.toLowerCase(), name: name.toLowerCase() }, 'source repository')
608
+ }
609
+
610
+ export function decodeZeropsSourceCredentialActivateRequest(value: unknown): ZeropsSourceCredentialActivateRequestV1 {
611
+ const parsed = record(value, 'source credential activate request')
612
+ exactKeys(
613
+ parsed,
614
+ ['protocolVersion', 'connectionId', 'credentialBundle', 'credentialSha256'],
615
+ 'source credential activate request',
616
+ )
617
+ const credentialBundle = boundedString(
618
+ parsed['credentialBundle'],
619
+ 'source credential activate request.credentialBundle',
620
+ MAX_SOURCE_CREDENTIAL_BUNDLE_BYTES,
621
+ )
622
+ decodeZeropsSourceCredentialBundle(credentialBundle)
623
+ return {
624
+ protocolVersion: protocolVersion(parsed, 'source credential activate request'),
625
+ connectionId: identifier(parsed['connectionId'], 'source credential activate request.connectionId'),
626
+ credentialBundle,
627
+ credentialSha256: sha256(parsed['credentialSha256'], 'source credential activate request.credentialSha256'),
628
+ }
629
+ }
630
+
631
+ export function buildZeropsSourceCredentialActivateRequest(
632
+ input: ZeropsSourceCredentialActivateInput,
633
+ ): ZeropsSourceCredentialActivateRequestV1 {
634
+ return decodeZeropsSourceCredentialActivateRequest({
635
+ protocolVersion: ZEROPS_SOURCE_PROTOCOL_VERSION,
636
+ connectionId: input.connectionId,
637
+ credentialBundle: input.credentialBundle,
638
+ credentialSha256: input.credentialSha256,
639
+ })
640
+ }
641
+
642
+ export function decodeZeropsSourceCredentialActivateResponse(value: unknown): ZeropsSourceCredentialActivateResponseV1 {
643
+ const parsed = record(value, 'source credential activate response')
644
+ exactKeys(
645
+ parsed,
646
+ ['protocolVersion', 'connectionId', 'credentialVersion', 'credentialSha256', 'githubApp'],
647
+ 'source credential activate response',
648
+ )
649
+ if (parsed['credentialVersion'] !== ZEROPS_SOURCE_CREDENTIAL_BUNDLE_VERSION) {
650
+ throw new Error('source credential activate response has an unsupported credentialVersion')
651
+ }
652
+ return {
653
+ protocolVersion: protocolVersion(parsed, 'source credential activate response'),
654
+ connectionId: identifier(parsed['connectionId'], 'source credential activate response.connectionId'),
655
+ credentialVersion: ZEROPS_SOURCE_CREDENTIAL_BUNDLE_VERSION,
656
+ credentialSha256: sha256(parsed['credentialSha256'], 'source credential activate response.credentialSha256'),
657
+ githubApp: githubAppIdentity(parsed['githubApp'], 'source credential activate response.githubApp'),
658
+ }
659
+ }
660
+
661
+ export function buildZeropsSourceCredentialActivateResponse(
662
+ input: Omit<ZeropsSourceCredentialActivateResponseV1, 'protocolVersion'>,
663
+ ): ZeropsSourceCredentialActivateResponseV1 {
664
+ return decodeZeropsSourceCredentialActivateResponse({ protocolVersion: ZEROPS_SOURCE_PROTOCOL_VERSION, ...input })
665
+ }
666
+
667
+ export function decodeZeropsSourceCredentialStatusRequest(value: unknown): ZeropsSourceCredentialStatusRequestV1 {
668
+ const parsed = record(value, 'source credential status request')
669
+ exactKeys(parsed, ['protocolVersion', 'connectionId'], 'source credential status request')
670
+ return {
671
+ protocolVersion: protocolVersion(parsed, 'source credential status request'),
672
+ connectionId: identifier(parsed['connectionId'], 'source credential status request.connectionId'),
673
+ }
674
+ }
675
+
676
+ export function buildZeropsSourceCredentialStatusRequest(
677
+ input: ZeropsSourceCredentialStatusInput,
678
+ ): ZeropsSourceCredentialStatusRequestV1 {
679
+ return decodeZeropsSourceCredentialStatusRequest({
680
+ protocolVersion: ZEROPS_SOURCE_PROTOCOL_VERSION,
681
+ connectionId: input.connectionId,
682
+ })
683
+ }
684
+
685
+ export function decodeZeropsSourceCredentialStatusResponse(value: unknown): ZeropsSourceCredentialStatusResponseV1 {
686
+ const parsed = record(value, 'source credential status response')
687
+ if (parsed['state'] === 'anonymous') {
688
+ exactKeys(parsed, ['protocolVersion', 'connectionId', 'state'], 'source credential status response')
689
+ return {
690
+ protocolVersion: protocolVersion(parsed, 'source credential status response'),
691
+ connectionId: identifier(parsed['connectionId'], 'source credential status response.connectionId'),
692
+ state: 'anonymous',
693
+ }
694
+ }
695
+ if (parsed['state'] !== 'active') throw new Error('source credential status response.state is invalid')
696
+ exactKeys(
697
+ parsed,
698
+ ['protocolVersion', 'connectionId', 'state', 'credentialVersion', 'credentialSha256', 'githubApp'],
699
+ 'source credential status response',
700
+ )
701
+ const active = decodeZeropsSourceCredentialActivateResponse({
702
+ protocolVersion: parsed['protocolVersion'],
703
+ connectionId: parsed['connectionId'],
704
+ credentialVersion: parsed['credentialVersion'],
705
+ credentialSha256: parsed['credentialSha256'],
706
+ githubApp: parsed['githubApp'],
707
+ })
708
+ return { ...active, state: 'active' }
709
+ }
710
+
711
+ export function buildZeropsSourceCredentialStatusResponse(
712
+ input:
713
+ | { readonly connectionId: string; readonly state: 'anonymous' }
714
+ | Omit<ZeropsSourceCredentialActiveStatusResponseV1, 'protocolVersion'>,
715
+ ): ZeropsSourceCredentialStatusResponseV1 {
716
+ return decodeZeropsSourceCredentialStatusResponse({ protocolVersion: ZEROPS_SOURCE_PROTOCOL_VERSION, ...input })
717
+ }
718
+
719
+ const webhookUrl = (value: unknown, label: string): string => {
720
+ const candidate = boundedString(value, label, MAX_WEBHOOK_URL_LENGTH)
721
+ let parsed: URL
722
+ try {
723
+ parsed = new URL(candidate)
724
+ } catch {
725
+ throw new Error(`${label} must be a secure URL`)
726
+ }
727
+ if (
728
+ parsed.protocol !== 'https:' || parsed.port !== '' || parsed.username !== '' || parsed.password !== '' || parsed.search !== '' || parsed.hash !== ''
729
+ || parsed.pathname !== '/webhooks/github' || parsed.hostname === '' || candidate !== parsed.href
730
+ ) throw new Error(`${label} must be an exact secure GitHub webhook URL`)
731
+ return candidate
732
+ }
733
+
734
+ const webhookSecret = (value: unknown, label: string): string => {
735
+ const secret = boundedString(value, label, MAX_WEBHOOK_SECRET_LENGTH)
736
+ if ([...secret].some((character) => character.charCodeAt(0) < 0x20 || character.charCodeAt(0) === 0x7f)) {
737
+ throw new Error(`${label} is invalid`)
738
+ }
739
+ return secret
740
+ }
741
+
742
+ export function decodeZeropsSourceWebhookConfigureRequest(value: unknown): ZeropsSourceWebhookConfigureRequestV1 {
743
+ const parsed = record(value, 'source webhook configure request')
744
+ exactKeys(parsed, ['protocolVersion', 'connectionId', 'credentialSha256', 'url', 'secret'], 'source webhook configure request')
745
+ return {
746
+ protocolVersion: protocolVersion(parsed, 'source webhook configure request'),
747
+ connectionId: identifier(parsed['connectionId'], 'source webhook configure request.connectionId'),
748
+ credentialSha256: sha256(parsed['credentialSha256'], 'source webhook configure request.credentialSha256'),
749
+ url: webhookUrl(parsed['url'], 'source webhook configure request.url'),
750
+ secret: webhookSecret(parsed['secret'], 'source webhook configure request.secret'),
751
+ }
752
+ }
753
+
754
+ export function buildZeropsSourceWebhookConfigureRequest(input: ZeropsSourceWebhookConfigureInput): ZeropsSourceWebhookConfigureRequestV1 {
755
+ return decodeZeropsSourceWebhookConfigureRequest({
756
+ protocolVersion: ZEROPS_SOURCE_PROTOCOL_VERSION,
757
+ connectionId: input.connectionId,
758
+ credentialSha256: input.credentialSha256,
759
+ url: input.url,
760
+ secret: input.secret,
761
+ })
762
+ }
763
+
764
+ export function decodeZeropsSourceWebhookConfigureResponse(value: unknown): ZeropsSourceWebhookConfigureResponseV1 {
765
+ const parsed = record(value, 'source webhook configure response')
766
+ exactKeys(parsed, ['protocolVersion', 'connectionId', 'credentialSha256', 'webhook'], 'source webhook configure response')
767
+ const webhook = record(parsed['webhook'], 'source webhook configure response.webhook')
768
+ exactKeys(webhook, ['url', 'contentType', 'insecureSsl'], 'source webhook configure response.webhook')
769
+ if (webhook['contentType'] !== 'json' || webhook['insecureSsl'] !== '0') throw new Error('source webhook configure response is insecure')
770
+ return {
771
+ protocolVersion: protocolVersion(parsed, 'source webhook configure response'),
772
+ connectionId: identifier(parsed['connectionId'], 'source webhook configure response.connectionId'),
773
+ credentialSha256: sha256(parsed['credentialSha256'], 'source webhook configure response.credentialSha256'),
774
+ webhook: { url: webhookUrl(webhook['url'], 'source webhook configure response.webhook.url'), contentType: 'json', insecureSsl: '0' },
775
+ }
776
+ }
777
+
778
+ export function buildZeropsSourceWebhookConfigureResponse(
779
+ input: Omit<ZeropsSourceWebhookConfigureResponseV1, 'protocolVersion'>,
780
+ ): ZeropsSourceWebhookConfigureResponseV1 {
781
+ return decodeZeropsSourceWebhookConfigureResponse({ protocolVersion: ZEROPS_SOURCE_PROTOCOL_VERSION, ...input })
782
+ }
783
+
784
+ const installationScope = (value: unknown, label: string): ZeropsSourceInstallationScopeV1 => {
785
+ const parsed = record(value, label)
786
+ if (parsed['kind'] === 'organization') {
787
+ exactKeys(parsed, ['kind', 'organization'], label)
788
+ const organization = boundedString(parsed['organization'], `${label}.organization`, 39).toLowerCase()
789
+ if (!GITHUB_OWNER_PATTERN.test(organization)) throw new Error(`${label}.organization is invalid`)
790
+ return { kind: 'organization', organization }
791
+ }
792
+ if (parsed['kind'] !== 'repositories') throw new Error(`${label}.kind is invalid`)
793
+ exactKeys(parsed, ['kind', 'repositories'], label)
794
+ if (!Array.isArray(parsed['repositories']) || parsed['repositories'].length === 0 || parsed['repositories'].length > MAX_INSTALLATION_REPOSITORIES) {
795
+ throw new Error(`${label}.repositories is invalid`)
796
+ }
797
+ const repositories = parsed['repositories'].map((entry) => repository(entry, `${label}.repositories entry`))
798
+ const keys = repositories.map((entry) => `${entry.owner}/${entry.name}`)
799
+ if (new Set(keys).size !== keys.length) throw new Error(`${label}.repositories contains a duplicate`)
800
+ return { kind: 'repositories', repositories }
801
+ }
802
+
803
+ export function decodeZeropsSourceInstallationsVerifyRequest(value: unknown): ZeropsSourceInstallationsVerifyRequestV1 {
804
+ const parsed = record(value, 'source installations verify request')
805
+ exactKeys(parsed, ['protocolVersion', 'connectionId', 'credentialSha256', 'scope'], 'source installations verify request')
806
+ return {
807
+ protocolVersion: protocolVersion(parsed, 'source installations verify request'),
808
+ connectionId: identifier(parsed['connectionId'], 'source installations verify request.connectionId'),
809
+ credentialSha256: sha256(parsed['credentialSha256'], 'source installations verify request.credentialSha256'),
810
+ scope: installationScope(parsed['scope'], 'source installations verify request.scope'),
811
+ }
812
+ }
813
+
814
+ export function buildZeropsSourceInstallationsVerifyRequest(
815
+ input: ZeropsSourceInstallationsVerifyInput,
816
+ ): ZeropsSourceInstallationsVerifyRequestV1 {
817
+ return decodeZeropsSourceInstallationsVerifyRequest({
818
+ protocolVersion: ZEROPS_SOURCE_PROTOCOL_VERSION,
819
+ connectionId: input.connectionId,
820
+ credentialSha256: input.credentialSha256,
821
+ scope: input.scope,
822
+ })
823
+ }
824
+
825
+ const installationVerification = (value: unknown, label: string): ZeropsSourceInstallationVerificationV1 => {
826
+ const parsed = record(value, label)
827
+ if (parsed['status'] === 'missing') {
828
+ exactKeys(parsed, ['status'], label)
829
+ return { status: 'missing' }
830
+ }
831
+ if (parsed['status'] !== 'installed') throw new Error(`${label}.status is invalid`)
832
+ exactKeys(parsed, ['status', 'installationId', 'accountLogin', 'repositorySelection'], label)
833
+ const accountLogin = boundedString(parsed['accountLogin'], `${label}.accountLogin`, 100)
834
+ if (!GITHUB_LOGIN_PATTERN.test(accountLogin)) throw new Error(`${label}.accountLogin is invalid`)
835
+ const repositorySelection = parsed['repositorySelection']
836
+ if (repositorySelection !== 'all' && repositorySelection !== 'selected') throw new Error(`${label}.repositorySelection is invalid`)
837
+ return {
838
+ status: 'installed',
839
+ installationId: installationId(parsed['installationId'], `${label}.installationId`),
840
+ accountLogin,
841
+ repositorySelection,
842
+ }
843
+ }
844
+
845
+ export function decodeZeropsSourceInstallationsVerifyResponse(value: unknown): ZeropsSourceInstallationsVerifyResponseV1 {
846
+ const parsed = record(value, 'source installations verify response')
847
+ exactKeys(parsed, ['protocolVersion', 'connectionId', 'credentialSha256', 'installation'], 'source installations verify response')
848
+ return {
849
+ protocolVersion: protocolVersion(parsed, 'source installations verify response'),
850
+ connectionId: identifier(parsed['connectionId'], 'source installations verify response.connectionId'),
851
+ credentialSha256: sha256(parsed['credentialSha256'], 'source installations verify response.credentialSha256'),
852
+ installation: installationVerification(parsed['installation'], 'source installations verify response.installation'),
853
+ }
854
+ }
855
+
856
+ export function buildZeropsSourceInstallationsVerifyResponse(
857
+ input: Omit<ZeropsSourceInstallationsVerifyResponseV1, 'protocolVersion'>,
858
+ ): ZeropsSourceInstallationsVerifyResponseV1 {
859
+ return decodeZeropsSourceInstallationsVerifyResponse({ protocolVersion: ZEROPS_SOURCE_PROTOCOL_VERSION, ...input })
860
+ }
861
+
862
+ export function decodeZeropsSourceResolveInstallationRequest(value: unknown): ZeropsSourceResolveInstallationRequestV1 {
863
+ const parsed = record(value, 'source resolve-installation request')
864
+ exactKeys(parsed, ['protocolVersion', 'repository'], 'source resolve-installation request')
865
+ return {
866
+ protocolVersion: protocolVersion(parsed, 'source resolve-installation request'),
867
+ repository: repository(parsed['repository'], 'source resolve-installation request.repository'),
868
+ }
869
+ }
870
+
871
+ export function buildZeropsSourceResolveInstallationRequest(repoUrl: string): ZeropsSourceResolveInstallationRequestV1 {
872
+ return decodeZeropsSourceResolveInstallationRequest({
873
+ protocolVersion: ZEROPS_SOURCE_PROTOCOL_VERSION,
874
+ repository: normalizeZeropsSourceRepository(repoUrl),
875
+ })
876
+ }
877
+
878
+ export function decodeZeropsSourceResolveInstallationResponse(value: unknown): ZeropsSourceResolveInstallationResponseV1 {
879
+ const parsed = record(value, 'source resolve-installation response')
880
+ exactKeys(parsed, ['protocolVersion', 'githubInstallationId'], 'source resolve-installation response')
881
+ return {
882
+ protocolVersion: protocolVersion(parsed, 'source resolve-installation response'),
883
+ githubInstallationId: parsed['githubInstallationId'] === null
884
+ ? null
885
+ : installationId(parsed['githubInstallationId'], 'source resolve-installation response.githubInstallationId'),
886
+ }
887
+ }
888
+
889
+ export function buildZeropsSourceResolveInstallationResponse(githubInstallationId: number | null): ZeropsSourceResolveInstallationResponseV1 {
890
+ return decodeZeropsSourceResolveInstallationResponse({ protocolVersion: ZEROPS_SOURCE_PROTOCOL_VERSION, githubInstallationId })
891
+ }
892
+
893
+ export function decodeZeropsSourceResolveRequest(value: unknown): ZeropsSourceResolveRequestV1 {
894
+ const parsed = record(value, 'source resolve request')
895
+ optionalFields(
896
+ parsed,
897
+ ['protocolVersion', 'runId', 'repository', 'requestedRef', 'descriptorSha256'],
898
+ ['expectedCommitSha', 'githubInstallationId'],
899
+ 'source resolve request',
900
+ )
901
+ const expected = optionalCommitSha(parsed['expectedCommitSha'], 'source resolve request.expectedCommitSha')
902
+ const installation = optionalInstallationId(parsed['githubInstallationId'], 'source resolve request.githubInstallationId')
903
+ return {
904
+ protocolVersion: protocolVersion(parsed, 'source resolve request'),
905
+ runId: identifier(parsed['runId'], 'source resolve request.runId'),
906
+ repository: repository(parsed['repository'], 'source resolve request.repository'),
907
+ requestedRef: ref(parsed['requestedRef'], 'source resolve request.requestedRef'),
908
+ ...(expected !== undefined ? { expectedCommitSha: expected } : {}),
909
+ ...(installation !== undefined ? { githubInstallationId: installation } : {}),
910
+ descriptorSha256: sha256(parsed['descriptorSha256'], 'source resolve request.descriptorSha256'),
911
+ }
912
+ }
913
+
914
+ export function buildZeropsSourceResolveRequest(input: ZeropsSourceResolveInput): ZeropsSourceResolveRequestV1 {
915
+ return decodeZeropsSourceResolveRequest({
916
+ protocolVersion: ZEROPS_SOURCE_PROTOCOL_VERSION,
917
+ runId: input.runId,
918
+ repository: input.repository,
919
+ requestedRef: input.requestedRef,
920
+ ...(input.expectedCommitSha !== undefined ? { expectedCommitSha: input.expectedCommitSha } : {}),
921
+ ...(input.githubInstallationId !== undefined ? { githubInstallationId: input.githubInstallationId } : {}),
922
+ descriptorSha256: input.descriptorSha256,
923
+ })
924
+ }
925
+
926
+ export function decodeZeropsSourceResolveResponse(value: unknown): ZeropsSourceResolveResponseV1 {
927
+ const parsed = record(value, 'source resolve response')
928
+ exactKeys(parsed, ['protocolVersion', 'runId', 'commitSha', 'descriptorSha256'], 'source resolve response')
929
+ return {
930
+ protocolVersion: protocolVersion(parsed, 'source resolve response'),
931
+ runId: identifier(parsed['runId'], 'source resolve response.runId'),
932
+ commitSha: commitSha(parsed['commitSha'], 'source resolve response.commitSha'),
933
+ descriptorSha256: sha256(parsed['descriptorSha256'], 'source resolve response.descriptorSha256'),
934
+ }
935
+ }
936
+
937
+ export function buildZeropsSourceResolveResponse(result: ZeropsSourceResolveResult): ZeropsSourceResolveResponseV1 {
938
+ return decodeZeropsSourceResolveResponse({ protocolVersion: ZEROPS_SOURCE_PROTOCOL_VERSION, ...result })
939
+ }
940
+
941
+ export function decodeZeropsSourceUploadRequest(value: unknown): ZeropsSourceUploadRequestV1 {
942
+ const parsed = record(value, 'source upload request')
943
+ optionalFields(
944
+ parsed,
945
+ ['protocolVersion', 'runId', 'appVersionId', 'repository', 'commitSha', 'uploadUrl', 'descriptor'],
946
+ ['githubInstallationId'],
947
+ 'source upload request',
948
+ )
949
+ const installation = optionalInstallationId(parsed['githubInstallationId'], 'source upload request.githubInstallationId')
950
+ return {
951
+ protocolVersion: protocolVersion(parsed, 'source upload request'),
952
+ runId: identifier(parsed['runId'], 'source upload request.runId'),
953
+ appVersionId: identifier(parsed['appVersionId'], 'source upload request.appVersionId'),
954
+ repository: repository(parsed['repository'], 'source upload request.repository'),
955
+ commitSha: commitSha(parsed['commitSha'], 'source upload request.commitSha'),
956
+ ...(installation !== undefined ? { githubInstallationId: installation } : {}),
957
+ uploadUrl: uploadUrl(parsed['uploadUrl'], 'source upload request.uploadUrl'),
958
+ descriptor: descriptor(parsed['descriptor'], 'source upload request.descriptor'),
959
+ }
960
+ }
961
+
962
+ export function buildZeropsSourceUploadRequest(input: ZeropsSourceUploadInput): ZeropsSourceUploadRequestV1 {
963
+ return decodeZeropsSourceUploadRequest({
964
+ protocolVersion: ZEROPS_SOURCE_PROTOCOL_VERSION,
965
+ runId: input.runId,
966
+ appVersionId: input.appVersionId,
967
+ repository: input.repository,
968
+ commitSha: input.commitSha,
969
+ ...(input.githubInstallationId !== undefined ? { githubInstallationId: input.githubInstallationId } : {}),
970
+ uploadUrl: input.uploadUrl,
971
+ descriptor: input.descriptor,
972
+ })
973
+ }
974
+
975
+ export function decodeZeropsSourceUploadResponse(value: unknown): ZeropsSourceUploadResponseV1 {
976
+ const parsed = record(value, 'source upload response')
977
+ exactKeys(parsed, ['protocolVersion', 'runId', 'appVersionId', 'commitSha', 'descriptorSha256'], 'source upload response')
978
+ return {
979
+ protocolVersion: protocolVersion(parsed, 'source upload response'),
980
+ runId: identifier(parsed['runId'], 'source upload response.runId'),
981
+ appVersionId: identifier(parsed['appVersionId'], 'source upload response.appVersionId'),
982
+ commitSha: commitSha(parsed['commitSha'], 'source upload response.commitSha'),
983
+ descriptorSha256: sha256(parsed['descriptorSha256'], 'source upload response.descriptorSha256'),
984
+ }
985
+ }
986
+
987
+ export function buildZeropsSourceUploadResponse(result: ZeropsSourceUploadResult): ZeropsSourceUploadResponseV1 {
988
+ return decodeZeropsSourceUploadResponse({ protocolVersion: ZEROPS_SOURCE_PROTOCOL_VERSION, ...result })
989
+ }
990
+
991
+ export function decodeZeropsSourceCancelRequest(value: unknown): ZeropsSourceCancelRequestV1 {
992
+ const parsed = record(value, 'source cancel request')
993
+ exactKeys(parsed, ['protocolVersion', 'runId', 'appVersionId'], 'source cancel request')
994
+ return {
995
+ protocolVersion: protocolVersion(parsed, 'source cancel request'),
996
+ runId: identifier(parsed['runId'], 'source cancel request.runId'),
997
+ appVersionId: identifier(parsed['appVersionId'], 'source cancel request.appVersionId'),
998
+ }
999
+ }
1000
+
1001
+ export function buildZeropsSourceCancelRequest(input: ZeropsSourceCancelInput): ZeropsSourceCancelRequestV1 {
1002
+ return decodeZeropsSourceCancelRequest({
1003
+ protocolVersion: ZEROPS_SOURCE_PROTOCOL_VERSION,
1004
+ runId: input.runId,
1005
+ appVersionId: input.appVersionId,
1006
+ })
1007
+ }
1008
+
1009
+ export function decodeZeropsSourceCancelResponse(value: unknown): ZeropsSourceCancelResponseV1 {
1010
+ const parsed = record(value, 'source cancel response')
1011
+ exactKeys(parsed, ['protocolVersion', 'runId', 'appVersionId'], 'source cancel response')
1012
+ return {
1013
+ protocolVersion: protocolVersion(parsed, 'source cancel response'),
1014
+ runId: identifier(parsed['runId'], 'source cancel response.runId'),
1015
+ appVersionId: identifier(parsed['appVersionId'], 'source cancel response.appVersionId'),
1016
+ }
1017
+ }
1018
+
1019
+ export function buildZeropsSourceCancelResponse(result: ZeropsSourceCancelResult): ZeropsSourceCancelResponseV1 {
1020
+ return decodeZeropsSourceCancelResponse({ protocolVersion: ZEROPS_SOURCE_PROTOCOL_VERSION, ...result })
1021
+ }
1022
+
1023
+ const includesString = <T extends string>(values: readonly T[], value: unknown): value is T =>
1024
+ typeof value === 'string' && values.some((entry) => entry === value)
1025
+
1026
+ export function decodeZeropsSourceErrorEnvelope(value: unknown): ZeropsSourceErrorEnvelope {
1027
+ const parsed = record(value, 'source error response')
1028
+ exactKeys(parsed, ['error'], 'source error response')
1029
+ const error = record(parsed['error'], 'source error response.error')
1030
+ exactKeys(error, ['code', 'stage', 'retryable'], 'source error response.error')
1031
+ if (!includesString(ZEROPS_SOURCE_ERROR_CODES, error['code'])) throw new Error('source error response.error.code is invalid')
1032
+ if (!includesString(ZEROPS_SOURCE_ERROR_STAGES, error['stage'])) throw new Error('source error response.error.stage is invalid')
1033
+ if (typeof error['retryable'] !== 'boolean') throw new Error('source error response.error.retryable must be boolean')
1034
+ return { error: { code: error['code'], stage: error['stage'], retryable: error['retryable'] } }
1035
+ }
1036
+
1037
+ export function buildZeropsSourceErrorEnvelope(
1038
+ code: ZeropsSourceErrorCode,
1039
+ stage: ZeropsSourceErrorStage,
1040
+ retryable: boolean,
1041
+ ): ZeropsSourceErrorEnvelope {
1042
+ return decodeZeropsSourceErrorEnvelope({ error: { code, stage, retryable } })
1043
+ }