@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
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
import type { ZeropsApi, ZeropsService, ZeropsServiceEnv } from './api'
|
|
2
|
+
import {
|
|
3
|
+
buildZeropsSourceCredentialBundle,
|
|
4
|
+
decodeZeropsSourceCredentialBundle,
|
|
5
|
+
serializeZeropsSourceCredentialBundle,
|
|
6
|
+
sha256ZeropsSourceCredentialBundle,
|
|
7
|
+
type ZeropsSourceCredentialActivateResponseV1,
|
|
8
|
+
type ZeropsSourceCredentialManager,
|
|
9
|
+
type ZeropsSourceCredentialStatusResponseV1,
|
|
10
|
+
type ZeropsSourceGitHubAppIdentityV1,
|
|
11
|
+
type ZeropsSourceInstallationsVerifyInput,
|
|
12
|
+
type ZeropsSourceInstallationsVerifyResponseV1,
|
|
13
|
+
type ZeropsSourceWebhookConfigureInput,
|
|
14
|
+
type ZeropsSourceWebhookConfigureResponseV1,
|
|
15
|
+
} from './source'
|
|
16
|
+
|
|
17
|
+
export const ZEROPS_SOURCE_CREDENTIAL_ENV = 'GITHUB_APP_CREDENTIALS'
|
|
18
|
+
export const ZEROPS_SOURCE_LEGACY_APP_ID_ENV = 'GITHUB_APP_ID'
|
|
19
|
+
export const ZEROPS_SOURCE_LEGACY_PRIVATE_KEY_ENV = 'GITHUB_APP_PRIVATE_KEY'
|
|
20
|
+
|
|
21
|
+
const SOURCE_HOSTNAME = 'source'
|
|
22
|
+
const MAX_ENV_ENTRIES = 256
|
|
23
|
+
const PROJECT_ID_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/
|
|
24
|
+
const DEFAULT_REREAD_ATTEMPTS = 5
|
|
25
|
+
const DEFAULT_REREAD_DELAY_MS = 250
|
|
26
|
+
const ADOPTION_CONNECTION_DOMAIN = 'fabrika:zerops-source-connection:v1'
|
|
27
|
+
|
|
28
|
+
export type SourceConnectionInspection =
|
|
29
|
+
| { readonly state: 'unavailable' }
|
|
30
|
+
| { readonly state: 'anonymous' }
|
|
31
|
+
| { readonly state: 'legacy-complete' }
|
|
32
|
+
| { readonly state: 'legacy-partial' }
|
|
33
|
+
| { readonly state: 'durable'; readonly credentialSha256: string }
|
|
34
|
+
|
|
35
|
+
export type SourceConnectionStatus =
|
|
36
|
+
| SourceConnectionInspection
|
|
37
|
+
| { readonly state: 'activation-required'; readonly credentialSha256: string }
|
|
38
|
+
| {
|
|
39
|
+
readonly state: 'active'
|
|
40
|
+
readonly credentialSha256: string
|
|
41
|
+
readonly githubApp: ZeropsSourceGitHubAppIdentityV1
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface SourceConnectionActivateInput {
|
|
45
|
+
readonly connectionId: string
|
|
46
|
+
readonly credentialBundle: string
|
|
47
|
+
readonly credentialSha256: string
|
|
48
|
+
readonly signal: AbortSignal
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface SourceConnectionStatusInput {
|
|
52
|
+
readonly connectionId: string
|
|
53
|
+
readonly signal: AbortSignal
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface SourceConnectionAdoptExistingInput {
|
|
57
|
+
readonly signal: AbortSignal
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Provider-neutral lifecycle consumed by the future authenticated control connection flow. */
|
|
61
|
+
export interface SourceConnectionAdmin {
|
|
62
|
+
inspect(signal: AbortSignal): Promise<SourceConnectionInspection>
|
|
63
|
+
adoptExisting(input: SourceConnectionAdoptExistingInput): Promise<ZeropsSourceCredentialActivateResponseV1>
|
|
64
|
+
activate(input: SourceConnectionActivateInput): Promise<ZeropsSourceCredentialActivateResponseV1>
|
|
65
|
+
status(input: SourceConnectionStatusInput): Promise<SourceConnectionStatus>
|
|
66
|
+
configureWebhook(input: ZeropsSourceWebhookConfigureInput): Promise<ZeropsSourceWebhookConfigureResponseV1>
|
|
67
|
+
verifyInstallations(input: ZeropsSourceInstallationsVerifyInput): Promise<ZeropsSourceInstallationsVerifyResponseV1>
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export type SourceConnectionZeropsApi = Pick<ZeropsApi, 'findService' | 'listServiceEnv' | 'createServiceEnv'>
|
|
71
|
+
|
|
72
|
+
export interface ZeropsSourceConnectionAdminOptions {
|
|
73
|
+
readonly api: SourceConnectionZeropsApi
|
|
74
|
+
readonly source: ZeropsSourceCredentialManager
|
|
75
|
+
readonly projectId: string
|
|
76
|
+
readonly reread?: {
|
|
77
|
+
readonly attempts?: number
|
|
78
|
+
readonly delayMs?: number
|
|
79
|
+
readonly sleep?: (delayMs: number, signal: AbortSignal) => Promise<void>
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export type SourceConnectionAdminErrorCode =
|
|
84
|
+
| 'invalid_configuration'
|
|
85
|
+
| 'source_not_found'
|
|
86
|
+
| 'source_mismatch'
|
|
87
|
+
| 'credential_conflict'
|
|
88
|
+
| 'credential_persistence'
|
|
89
|
+
| 'credential_activation'
|
|
90
|
+
|
|
91
|
+
/** Stable detail-free failure. It never carries a project id, service id, environment value, or upstream body. */
|
|
92
|
+
export class SourceConnectionAdminError extends Error {
|
|
93
|
+
constructor(readonly code: SourceConnectionAdminErrorCode) {
|
|
94
|
+
super(`source connection administration failed (${code})`)
|
|
95
|
+
this.name = 'SourceConnectionAdminError'
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
interface DurableInspection {
|
|
100
|
+
readonly public: SourceConnectionInspection
|
|
101
|
+
readonly service: ZeropsService
|
|
102
|
+
readonly bundle?: string
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function createZeropsSourceConnectionAdmin(options: ZeropsSourceConnectionAdminOptions): SourceConnectionAdmin {
|
|
106
|
+
if (!PROJECT_ID_PATTERN.test(options.projectId)) throw new SourceConnectionAdminError('invalid_configuration')
|
|
107
|
+
const attempts = options.reread?.attempts ?? DEFAULT_REREAD_ATTEMPTS
|
|
108
|
+
const delayMs = options.reread?.delayMs ?? DEFAULT_REREAD_DELAY_MS
|
|
109
|
+
if (!Number.isSafeInteger(attempts) || attempts <= 0 || attempts > 20 || !Number.isSafeInteger(delayMs) || delayMs < 0 || delayMs > 5_000) {
|
|
110
|
+
throw new SourceConnectionAdminError('invalid_configuration')
|
|
111
|
+
}
|
|
112
|
+
const sleep = options.reread?.sleep ?? defaultSleep
|
|
113
|
+
|
|
114
|
+
const inspectInternal = async (signal: AbortSignal): Promise<DurableInspection> => {
|
|
115
|
+
throwIfAborted(signal)
|
|
116
|
+
const service = await exactSourceService(options.api, options.projectId, signal)
|
|
117
|
+
let environment: ZeropsServiceEnv[]
|
|
118
|
+
try {
|
|
119
|
+
environment = await options.api.listServiceEnv({ serviceId: service.id, signal })
|
|
120
|
+
} catch (error) {
|
|
121
|
+
if (isAbort(error, signal)) throw error
|
|
122
|
+
throw new SourceConnectionAdminError('credential_persistence')
|
|
123
|
+
}
|
|
124
|
+
throwIfAborted(signal)
|
|
125
|
+
return await withDigest(classify(service, environment))
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const admin: SourceConnectionAdmin = {
|
|
129
|
+
inspect: async (signal) => (await inspectInternal(signal)).public,
|
|
130
|
+
|
|
131
|
+
adoptExisting: async (input) => {
|
|
132
|
+
const existing = await inspectInternal(input.signal)
|
|
133
|
+
if (
|
|
134
|
+
(existing.public.state !== 'legacy-complete' && existing.public.state !== 'durable')
|
|
135
|
+
|| existing.bundle === undefined
|
|
136
|
+
) throw new SourceConnectionAdminError('credential_conflict')
|
|
137
|
+
let credentialSha256: string
|
|
138
|
+
try {
|
|
139
|
+
credentialSha256 = existing.public.state === 'durable'
|
|
140
|
+
? existing.public.credentialSha256
|
|
141
|
+
: await sha256ZeropsSourceCredentialBundle(existing.bundle)
|
|
142
|
+
} catch {
|
|
143
|
+
throw new SourceConnectionAdminError('credential_conflict')
|
|
144
|
+
}
|
|
145
|
+
return await admin.activate({
|
|
146
|
+
connectionId: await adoptionConnectionId(options.projectId, credentialSha256),
|
|
147
|
+
credentialBundle: existing.bundle,
|
|
148
|
+
credentialSha256,
|
|
149
|
+
signal: input.signal,
|
|
150
|
+
})
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
activate: async (input) => {
|
|
154
|
+
let digest: string
|
|
155
|
+
try {
|
|
156
|
+
decodeZeropsSourceCredentialBundle(input.credentialBundle)
|
|
157
|
+
digest = await sha256ZeropsSourceCredentialBundle(input.credentialBundle)
|
|
158
|
+
} catch {
|
|
159
|
+
throw new SourceConnectionAdminError('credential_conflict')
|
|
160
|
+
}
|
|
161
|
+
if (digest !== input.credentialSha256) throw new SourceConnectionAdminError('credential_conflict')
|
|
162
|
+
const initial = await inspectInternal(input.signal)
|
|
163
|
+
if (initial.public.state === 'legacy-partial' || initial.public.state === 'unavailable') {
|
|
164
|
+
throw new SourceConnectionAdminError('credential_conflict')
|
|
165
|
+
}
|
|
166
|
+
if (initial.public.state === 'durable') {
|
|
167
|
+
if (initial.bundle !== input.credentialBundle || initial.public.credentialSha256 !== digest) {
|
|
168
|
+
throw new SourceConnectionAdminError('credential_conflict')
|
|
169
|
+
}
|
|
170
|
+
} else {
|
|
171
|
+
if (initial.public.state === 'legacy-complete' && initial.bundle !== input.credentialBundle) {
|
|
172
|
+
throw new SourceConnectionAdminError('credential_conflict')
|
|
173
|
+
}
|
|
174
|
+
try {
|
|
175
|
+
await options.api.createServiceEnv({
|
|
176
|
+
serviceId: initial.service.id,
|
|
177
|
+
key: ZEROPS_SOURCE_CREDENTIAL_ENV,
|
|
178
|
+
value: input.credentialBundle,
|
|
179
|
+
signal: input.signal,
|
|
180
|
+
})
|
|
181
|
+
} catch (error) {
|
|
182
|
+
if (isAbort(error, input.signal)) throw error
|
|
183
|
+
}
|
|
184
|
+
await proveDurable(input.credentialBundle, digest, input.signal, attempts, delayMs, sleep, inspectInternal)
|
|
185
|
+
}
|
|
186
|
+
let activated: ZeropsSourceCredentialActivateResponseV1
|
|
187
|
+
try {
|
|
188
|
+
activated = await options.source.activate(input)
|
|
189
|
+
} catch (error) {
|
|
190
|
+
if (isAbort(error, input.signal)) throw error
|
|
191
|
+
let status: ZeropsSourceCredentialStatusResponseV1
|
|
192
|
+
try {
|
|
193
|
+
status = await options.source.status({ connectionId: input.connectionId, signal: input.signal })
|
|
194
|
+
} catch (statusError) {
|
|
195
|
+
if (isAbort(statusError, input.signal)) throw statusError
|
|
196
|
+
throw new SourceConnectionAdminError('credential_activation')
|
|
197
|
+
}
|
|
198
|
+
if (
|
|
199
|
+
status.state !== 'active' || status.connectionId !== input.connectionId || status.credentialSha256 !== digest
|
|
200
|
+
) throw new SourceConnectionAdminError('credential_activation')
|
|
201
|
+
activated = {
|
|
202
|
+
protocolVersion: 1,
|
|
203
|
+
connectionId: input.connectionId,
|
|
204
|
+
credentialVersion: status.credentialVersion,
|
|
205
|
+
credentialSha256: status.credentialSha256,
|
|
206
|
+
githubApp: status.githubApp,
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (activated.connectionId !== input.connectionId || activated.credentialSha256 !== digest) {
|
|
210
|
+
throw new SourceConnectionAdminError('credential_activation')
|
|
211
|
+
}
|
|
212
|
+
return activated
|
|
213
|
+
},
|
|
214
|
+
|
|
215
|
+
status: async (input) => {
|
|
216
|
+
const durable = await inspectInternal(input.signal)
|
|
217
|
+
if (durable.public.state !== 'durable') return durable.public
|
|
218
|
+
let runtime: ZeropsSourceCredentialStatusResponseV1
|
|
219
|
+
try {
|
|
220
|
+
runtime = await options.source.status(input)
|
|
221
|
+
} catch (error) {
|
|
222
|
+
if (isAbort(error, input.signal)) throw error
|
|
223
|
+
throw new SourceConnectionAdminError('credential_activation')
|
|
224
|
+
}
|
|
225
|
+
if (runtime.connectionId !== input.connectionId || runtime.state === 'anonymous') {
|
|
226
|
+
return { state: 'activation-required', credentialSha256: durable.public.credentialSha256 }
|
|
227
|
+
}
|
|
228
|
+
if (runtime.credentialSha256 !== durable.public.credentialSha256) {
|
|
229
|
+
throw new SourceConnectionAdminError('credential_conflict')
|
|
230
|
+
}
|
|
231
|
+
return { state: 'active', credentialSha256: runtime.credentialSha256, githubApp: runtime.githubApp }
|
|
232
|
+
},
|
|
233
|
+
|
|
234
|
+
configureWebhook: async (input) => {
|
|
235
|
+
await requireDurableDigest(input.credentialSha256, input.signal, inspectInternal)
|
|
236
|
+
try {
|
|
237
|
+
const response = await options.source.configureWebhook(input)
|
|
238
|
+
if (
|
|
239
|
+
response.connectionId !== input.connectionId || response.credentialSha256 !== input.credentialSha256
|
|
240
|
+
|| response.webhook.url !== input.url || response.webhook.contentType !== 'json' || response.webhook.insecureSsl !== '0'
|
|
241
|
+
) throw new SourceConnectionAdminError('credential_activation')
|
|
242
|
+
return response
|
|
243
|
+
} catch (error) {
|
|
244
|
+
if (isAbort(error, input.signal)) throw error
|
|
245
|
+
if (error instanceof SourceConnectionAdminError) throw error
|
|
246
|
+
throw new SourceConnectionAdminError('credential_activation')
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
|
|
250
|
+
verifyInstallations: async (input) => {
|
|
251
|
+
await requireDurableDigest(input.credentialSha256, input.signal, inspectInternal)
|
|
252
|
+
try {
|
|
253
|
+
const response = await options.source.verifyInstallations(input)
|
|
254
|
+
if (response.connectionId !== input.connectionId || response.credentialSha256 !== input.credentialSha256) {
|
|
255
|
+
throw new SourceConnectionAdminError('credential_activation')
|
|
256
|
+
}
|
|
257
|
+
return response
|
|
258
|
+
} catch (error) {
|
|
259
|
+
if (isAbort(error, input.signal)) throw error
|
|
260
|
+
if (error instanceof SourceConnectionAdminError) throw error
|
|
261
|
+
throw new SourceConnectionAdminError('credential_activation')
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
}
|
|
265
|
+
return admin
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async function adoptionConnectionId(projectId: string, credentialSha256: string): Promise<string> {
|
|
269
|
+
const material = `${ADOPTION_CONNECTION_DOMAIN}\0${projectId}\0${credentialSha256}`
|
|
270
|
+
const digest = new Uint8Array(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(material)))
|
|
271
|
+
return `zsrc-${[...digest].map((byte) => byte.toString(16).padStart(2, '0')).join('')}`
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async function requireDurableDigest(
|
|
275
|
+
digest: string,
|
|
276
|
+
signal: AbortSignal,
|
|
277
|
+
inspect: (signal: AbortSignal) => Promise<DurableInspection>,
|
|
278
|
+
): Promise<void> {
|
|
279
|
+
const durable = await inspect(signal)
|
|
280
|
+
if (durable.public.state !== 'durable' || durable.public.credentialSha256 !== digest) {
|
|
281
|
+
throw new SourceConnectionAdminError('credential_conflict')
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async function exactSourceService(api: SourceConnectionZeropsApi, projectId: string, signal: AbortSignal): Promise<ZeropsService> {
|
|
286
|
+
let service: ZeropsService | null
|
|
287
|
+
try {
|
|
288
|
+
service = await api.findService({ projectId, hostname: SOURCE_HOSTNAME, signal })
|
|
289
|
+
} catch (error) {
|
|
290
|
+
if (isAbort(error, signal)) throw error
|
|
291
|
+
throw new SourceConnectionAdminError('source_not_found')
|
|
292
|
+
}
|
|
293
|
+
if (service === null) throw new SourceConnectionAdminError('source_not_found')
|
|
294
|
+
if (service.id === '' || service.name !== SOURCE_HOSTNAME || service.projectId !== projectId) {
|
|
295
|
+
throw new SourceConnectionAdminError('source_mismatch')
|
|
296
|
+
}
|
|
297
|
+
return service
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function classify(service: ZeropsService, environment: readonly ZeropsServiceEnv[]): DurableInspection {
|
|
301
|
+
if (environment.length > MAX_ENV_ENTRIES) throw new SourceConnectionAdminError('source_mismatch')
|
|
302
|
+
const bundleEntries = environment.filter((entry) => entry.key === ZEROPS_SOURCE_CREDENTIAL_ENV)
|
|
303
|
+
const legacyIdEntries = environment.filter((entry) => entry.key === ZEROPS_SOURCE_LEGACY_APP_ID_ENV)
|
|
304
|
+
const legacyKeyEntries = environment.filter((entry) => entry.key === ZEROPS_SOURCE_LEGACY_PRIVATE_KEY_ENV)
|
|
305
|
+
if (bundleEntries.length > 1 || legacyIdEntries.length > 1 || legacyKeyEntries.length > 1) {
|
|
306
|
+
throw new SourceConnectionAdminError('credential_conflict')
|
|
307
|
+
}
|
|
308
|
+
const legacyCount = legacyIdEntries.length + legacyKeyEntries.length
|
|
309
|
+
let legacyBundle: string | undefined
|
|
310
|
+
if (legacyIdEntries.length === 1 && legacyKeyEntries.length === 1) {
|
|
311
|
+
const legacyAppId = legacyIdEntries[0]?.content
|
|
312
|
+
const legacyPrivateKeyPem = legacyKeyEntries[0]?.content
|
|
313
|
+
if (legacyAppId === undefined || legacyPrivateKeyPem === undefined) throw new SourceConnectionAdminError('credential_conflict')
|
|
314
|
+
try {
|
|
315
|
+
legacyBundle = serializeZeropsSourceCredentialBundle(buildZeropsSourceCredentialBundle({
|
|
316
|
+
githubAppId: legacyAppId,
|
|
317
|
+
privateKeyPem: legacyPrivateKeyPem,
|
|
318
|
+
}))
|
|
319
|
+
} catch {
|
|
320
|
+
throw new SourceConnectionAdminError('credential_conflict')
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
if (bundleEntries.length === 1) {
|
|
324
|
+
if (legacyCount !== 0 && legacyBundle === undefined) throw new SourceConnectionAdminError('credential_conflict')
|
|
325
|
+
const bundle = bundleEntries[0]?.content
|
|
326
|
+
if (bundle === undefined) throw new SourceConnectionAdminError('credential_conflict')
|
|
327
|
+
try {
|
|
328
|
+
decodeZeropsSourceCredentialBundle(bundle)
|
|
329
|
+
} catch {
|
|
330
|
+
throw new SourceConnectionAdminError('credential_conflict')
|
|
331
|
+
}
|
|
332
|
+
if (legacyBundle !== undefined && legacyBundle !== bundle) throw new SourceConnectionAdminError('credential_conflict')
|
|
333
|
+
return { public: { state: 'durable', credentialSha256: '' }, service, bundle }
|
|
334
|
+
}
|
|
335
|
+
if (legacyCount === 0) return { public: { state: 'anonymous' }, service }
|
|
336
|
+
if (legacyBundle !== undefined) return { public: { state: 'legacy-complete' }, service, bundle: legacyBundle }
|
|
337
|
+
return { public: { state: 'legacy-partial' }, service }
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
async function withDigest(inspection: DurableInspection): Promise<DurableInspection> {
|
|
341
|
+
if (inspection.public.state !== 'durable' || inspection.bundle === undefined) return inspection
|
|
342
|
+
return {
|
|
343
|
+
...inspection,
|
|
344
|
+
public: { state: 'durable', credentialSha256: await sha256ZeropsSourceCredentialBundle(inspection.bundle) },
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async function proveDurable(
|
|
349
|
+
bundle: string,
|
|
350
|
+
digest: string,
|
|
351
|
+
signal: AbortSignal,
|
|
352
|
+
attempts: number,
|
|
353
|
+
delayMs: number,
|
|
354
|
+
sleep: (delayMs: number, signal: AbortSignal) => Promise<void>,
|
|
355
|
+
inspect: (signal: AbortSignal) => Promise<DurableInspection>,
|
|
356
|
+
): Promise<void> {
|
|
357
|
+
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
358
|
+
const durable = await withDigest(await inspect(signal))
|
|
359
|
+
if (durable.public.state === 'durable') {
|
|
360
|
+
if (durable.bundle === bundle && durable.public.credentialSha256 === digest) return
|
|
361
|
+
throw new SourceConnectionAdminError('credential_conflict')
|
|
362
|
+
}
|
|
363
|
+
if (durable.public.state !== 'anonymous') throw new SourceConnectionAdminError('credential_conflict')
|
|
364
|
+
if (attempt + 1 < attempts) await sleep(delayMs, signal)
|
|
365
|
+
}
|
|
366
|
+
throw new SourceConnectionAdminError('credential_persistence')
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const defaultSleep = (delayMs: number, signal: AbortSignal): Promise<void> => {
|
|
370
|
+
throwIfAborted(signal)
|
|
371
|
+
return new Promise((resolve, reject) => {
|
|
372
|
+
const timer = setTimeout(() => {
|
|
373
|
+
cleanup()
|
|
374
|
+
resolve()
|
|
375
|
+
}, delayMs)
|
|
376
|
+
const abort = (): void => {
|
|
377
|
+
cleanup()
|
|
378
|
+
reject(abortError())
|
|
379
|
+
}
|
|
380
|
+
const cleanup = (): void => {
|
|
381
|
+
clearTimeout(timer)
|
|
382
|
+
signal.removeEventListener('abort', abort)
|
|
383
|
+
}
|
|
384
|
+
signal.addEventListener('abort', abort, { once: true })
|
|
385
|
+
})
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const throwIfAborted = (signal: AbortSignal): void => {
|
|
389
|
+
if (signal.aborted) throw abortError()
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const isAbort = (error: unknown, signal: AbortSignal): boolean => signal.aborted || (error instanceof Error && error.name === 'AbortError')
|
|
393
|
+
|
|
394
|
+
const abortError = (): DOMException => new DOMException('Source connection administration was aborted', 'AbortError')
|