@fabrika/provider-zerops 0.0.4 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -5
- package/src/api.ts +137 -12
- package/src/cli.ts +42 -2
- package/src/codec.ts +70 -12
- package/src/collaborators.ts +5 -0
- package/src/control.ts +302 -43
- package/src/index.ts +119 -1
- package/src/manifest.ts +60 -3
- package/src/provider.ts +171 -31
- package/src/source-connection.ts +394 -0
- package/src/source.ts +1043 -0
- package/src/types.ts +18 -1
package/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
|
+
}
|