dsh-edge 0.3.0-alpha.1 → 0.3.0

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.
@@ -152,6 +152,13 @@ export class InstallerOutputError extends Error {
152
152
  }
153
153
  }
154
154
 
155
+ class R2SubscriptionUnavailableError extends Error {
156
+ constructor(message) {
157
+ super(message)
158
+ this.name = 'R2SubscriptionUnavailableError'
159
+ }
160
+ }
161
+
155
162
  /** Return account choices permitted by the selected runtime. */
156
163
  export function accountChoices(mode, accounts, command = 'install') {
157
164
  requireRuntimeMode(mode)
@@ -243,6 +250,10 @@ export function attachmentBucketName(workerName) {
243
250
  return `${prefix}-${digest}${suffix}`
244
251
  }
245
252
 
253
+ function r2ActivationUrl(accountId) {
254
+ return `https://dash.cloudflare.com/${encodeURIComponent(accountId)}/r2/overview`
255
+ }
256
+
246
257
  /** Create or reuse the permanent deployment's private R2 attachment bucket. */
247
258
  export async function ensureR2Bucket({
248
259
  bucketName,
@@ -253,6 +264,7 @@ export async function ensureR2Bucket({
253
264
  }) {
254
265
  const infoArgs = ['r2', 'bucket', 'info', bucketName, '--json', ...profileArgs(profile)]
255
266
  const initial = await runWrangler(infoArgs, { environment, signal })
267
+ throwIfR2SubscriptionUnavailable(initial)
256
268
  if (initial.status === 0) {
257
269
  requireR2BucketInfo(initial.stdout, bucketName)
258
270
  return { bucketName, created: false }
@@ -260,19 +272,38 @@ export async function ensureR2Bucket({
260
272
  const created = await runWrangler([
261
273
  'r2', 'bucket', 'create', bucketName, ...profileArgs(profile),
262
274
  ], { environment, signal })
275
+ throwIfR2SubscriptionUnavailable(created)
263
276
  if (created.status === 0) return { bucketName, created: true }
264
277
  // A concurrent installer may have won the create race; prove exact existence.
265
278
  const recovered = await runWrangler(infoArgs, { environment, signal })
279
+ throwIfR2SubscriptionUnavailable(recovered)
266
280
  if (recovered.status === 0) {
267
281
  requireR2BucketInfo(recovered.stdout, bucketName)
268
282
  return { bucketName, created: false }
269
283
  }
270
284
  throw new Error(commandFailure(
271
- `Could not create or access private R2 bucket "${bucketName}". Enable R2 for this Cloudflare account (or use a temporary preview), then retry`,
285
+ `Could not create or access private R2 bucket "${bucketName}". Check this account's R2 access and permissions, then retry`,
272
286
  created,
273
287
  ))
274
288
  }
275
289
 
290
+ /** Verify R2 availability without creating a bucket or collecting deployment credentials. */
291
+ async function verifyR2Subscription({ runWrangler, environment, profile, signal }) {
292
+ const result = await runWrangler([
293
+ 'r2', 'bucket', 'list', ...profileArgs(profile),
294
+ ], { environment, signal })
295
+ throwIfR2SubscriptionUnavailable(result)
296
+ requireSuccess(result, 'Could not check Cloudflare R2 availability')
297
+ }
298
+
299
+ function throwIfR2SubscriptionUnavailable(result) {
300
+ if (!/\[code:\s*10042\]/u.test(`${result.stdout}\n${result.stderr}`)) return
301
+ throw new R2SubscriptionUnavailableError(commandFailure(
302
+ 'Cloudflare R2 is not enabled for this account',
303
+ result,
304
+ ))
305
+ }
306
+
276
307
  function requireR2BucketInfo(source, expectedName) {
277
308
  let info
278
309
  try {
@@ -455,9 +486,11 @@ function versionAttachmentStorage(source) {
455
486
  || (marker === 'private-r2' && attachment.length !== 1)) {
456
487
  throw new Error('The existing Worker attachment marker does not match its binding.')
457
488
  }
458
- // Releases before attachment support had neither binding nor marker. An
459
- // authenticated upgrade can safely initialize those instances on private R2.
460
- return marker ?? 'private-r2'
489
+ if (marker !== undefined) return marker
490
+ // An R2 binding predating the explicit marker is still authoritative. A
491
+ // release with neither binding nor marker predates image attachments, so it
492
+ // has no image references to strand and can ask the owner to choose once.
493
+ return attachment.length === 1 ? 'private-r2' : undefined
461
494
  }
462
495
 
463
496
  /** Run the complete guided install with UI and Wrangler supplied as replaceable boundaries. */
@@ -552,7 +585,7 @@ export async function installEdge({
552
585
  const commandEnvironment = temporary
553
586
  ? unauthenticatedEnvironment(environment)
554
587
  : accountEnvironment(profileEnvironment ?? environment, account.id)
555
- const attachmentStorage = updatingExisting
588
+ const existingAttachmentStorage = updatingExisting
556
589
  ? await detectExistingAttachmentStorage({
557
590
  workerName,
558
591
  mode,
@@ -561,17 +594,34 @@ export async function installEdge({
561
594
  profile,
562
595
  signal,
563
596
  })
597
+ : undefined
598
+ let attachmentStorage = updatingExisting
599
+ ? existingAttachmentStorage ?? await ui.selectInitialAttachmentStorage()
564
600
  : temporary ? 'temporary-do' : 'private-r2'
601
+ requireAttachmentStorage(attachmentStorage)
565
602
 
566
- const secretMode = await ui.selectOwnerSecretMode()
567
- const ownerSecret = secretMode === 'generate'
568
- ? generateOwnerSecret()
569
- : await ui.ownerSecret(validateOwnerSecret)
570
- const secretError = validateOwnerSecret(ownerSecret)
571
- if (secretError !== undefined) throw new Error(secretError)
572
- const deepSeekKey = await ui.deepSeekKey(validateDeepSeekKey)
573
- const deepSeekError = validateDeepSeekKey(deepSeekKey)
574
- if (deepSeekError !== undefined) throw new Error(deepSeekError)
603
+ const canSwitchToDurableObject = updatingExisting
604
+ && existingAttachmentStorage === undefined
605
+ while (attachmentStorage === 'private-r2') {
606
+ ui.step('Checking Cloudflare R2 availability…')
607
+ try {
608
+ await verifyR2Subscription({
609
+ runWrangler,
610
+ environment: commandEnvironment,
611
+ profile,
612
+ signal,
613
+ })
614
+ break
615
+ } catch (error) {
616
+ if (!(error instanceof R2SubscriptionUnavailableError)) throw error
617
+ const resolution = await promptR2Recovery(
618
+ ui,
619
+ account.id,
620
+ canSwitchToDurableObject,
621
+ )
622
+ if (resolution === 'temporary-do') attachmentStorage = 'temporary-do'
623
+ }
624
+ }
575
625
 
576
626
  const confirmed = await ui.confirm({
577
627
  mode,
@@ -587,17 +637,44 @@ export async function installEdge({
587
637
  throw new InstallCancelledError()
588
638
  }
589
639
 
640
+ const secretMode = await ui.selectOwnerSecretMode()
641
+ const ownerSecret = secretMode === 'generate'
642
+ ? generateOwnerSecret()
643
+ : await ui.ownerSecret(validateOwnerSecret)
644
+ const secretError = validateOwnerSecret(ownerSecret)
645
+ if (secretError !== undefined) throw new Error(secretError)
646
+ const deepSeekKey = await ui.deepSeekKey(validateDeepSeekKey)
647
+ const deepSeekError = validateDeepSeekKey(deepSeekKey)
648
+ if (deepSeekError !== undefined) throw new Error(deepSeekError)
649
+
590
650
  let bucketName
591
651
  if (attachmentStorage === 'private-r2') {
592
652
  bucketName = attachmentBucketName(workerName)
593
653
  ui.step(`Preparing private image storage (${bucketName})…`)
594
- await ensureR2Bucket({
595
- bucketName,
596
- runWrangler,
597
- environment: commandEnvironment,
598
- profile,
599
- signal,
600
- })
654
+ while (true) {
655
+ try {
656
+ await ensureR2Bucket({
657
+ bucketName,
658
+ runWrangler,
659
+ environment: commandEnvironment,
660
+ profile,
661
+ signal,
662
+ })
663
+ break
664
+ } catch (error) {
665
+ if (!(error instanceof R2SubscriptionUnavailableError)) throw error
666
+ const resolution = await promptR2Recovery(
667
+ ui,
668
+ account.id,
669
+ canSwitchToDurableObject,
670
+ )
671
+ if (resolution === 'temporary-do') {
672
+ attachmentStorage = 'temporary-do'
673
+ bucketName = undefined
674
+ break
675
+ }
676
+ }
677
+ }
601
678
  }
602
679
 
603
680
  temporaryDirectory = await createTemporaryDirectory()
@@ -1162,16 +1239,48 @@ function requireRuntimeMode(mode) {
1162
1239
  }
1163
1240
  }
1164
1241
 
1242
+ function requireAttachmentStorage(storage) {
1243
+ if (storage !== 'temporary-do' && storage !== 'private-r2') {
1244
+ throw new Error(`Unsupported attachment storage: ${String(storage)}`)
1245
+ }
1246
+ }
1247
+
1248
+ function requireR2RecoveryAction(action, canSwitchToDurableObject) {
1249
+ if (action === 'cancel') throw new InstallCancelledError()
1250
+ if (action === 'retry') return action
1251
+ if (action === 'temporary-do' && canSwitchToDurableObject) return action
1252
+ throw new Error(`Unsupported R2 recovery action: ${String(action)}`)
1253
+ }
1254
+
1255
+ async function promptR2Recovery(ui, accountId, canSwitchToDurableObject) {
1256
+ return requireR2RecoveryAction(
1257
+ await ui.r2SubscriptionUnavailable({
1258
+ activationUrl: r2ActivationUrl(accountId),
1259
+ canSwitchToDurableObject,
1260
+ }),
1261
+ canSwitchToDurableObject,
1262
+ )
1263
+ }
1264
+
1165
1265
  function requireSuccess(result, prefix) {
1166
1266
  if (result.outputFailure !== undefined) throw result.outputFailure
1167
1267
  if (result.status !== 0) throw new Error(commandFailure(prefix, result))
1168
1268
  }
1169
1269
 
1170
1270
  function commandFailure(prefix, result) {
1171
- const detail = stripAnsi(result.stderr || result.stdout).trim()
1271
+ const detail = conciseDiagnostic(result.stderr || result.stdout)
1172
1272
  return detail === '' ? `${prefix}.` : `${prefix}: ${detail}`
1173
1273
  }
1174
1274
 
1275
+ /** Remove known Node dependency noise from concise errors while verbose output stays unchanged. */
1276
+ function conciseDiagnostic(value) {
1277
+ return stripAnsi(value).split(/\r?\n/u)
1278
+ .filter(line => !/^\(node:\d+\) \[DEP0040\] DeprecationWarning: The `punycode` module is deprecated\./u.test(line.trim()))
1279
+ .filter(line => !/^\(Use `node --trace-deprecation \.\.\.` to show where the warning was created\)$/u.test(line.trim()))
1280
+ .join('\n')
1281
+ .trim()
1282
+ }
1283
+
1175
1284
  function describeError(error) {
1176
1285
  return error instanceof Error ? error.message : String(error)
1177
1286
  }