buckyos 0.7.119 → 0.7.121

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.
Files changed (44) hide show
  1. package/cli/README.md +24 -3
  2. package/cli/core/app.ts +28 -0
  3. package/cli/deno.json +2 -1
  4. package/cli/dist/cli.mjs +777 -169
  5. package/cli/dist/cli.mjs.map +1 -1
  6. package/cli/modules/app.ts +213 -94
  7. package/cli/modules/provision.ts +690 -0
  8. package/cli/runtime/host.ts +25 -2
  9. package/cli/runtime/host_deno.ts +4 -0
  10. package/cli/runtime/host_node.ts +4 -0
  11. package/cli/version.ts +1 -1
  12. package/dist/browser.cjs +1 -1
  13. package/dist/browser.mjs +124 -124
  14. package/dist/index.cjs +1 -1
  15. package/dist/index.mjs +124 -124
  16. package/dist/{ndm_proxy-c00bc561.mjs → ndm_proxy-8f547002.mjs} +113 -216
  17. package/dist/ndm_proxy-8f547002.mjs.map +1 -0
  18. package/dist/ndm_proxy-93a8869b.js +13 -0
  19. package/dist/ndm_proxy-93a8869b.js.map +1 -0
  20. package/dist/ndn_types-44c69036.js +2 -0
  21. package/dist/ndn_types-44c69036.js.map +1 -0
  22. package/dist/{ndn_types-f6c08d20.mjs → ndn_types-d768245f.mjs} +149 -39
  23. package/dist/ndn_types-d768245f.mjs.map +1 -0
  24. package/dist/node.cjs +1 -1
  25. package/dist/node.mjs +124 -124
  26. package/dist/provision.cjs +6 -6
  27. package/dist/provision.cjs.map +1 -1
  28. package/dist/provision.d.ts +2 -0
  29. package/dist/provision.d.ts.map +1 -1
  30. package/dist/provision.mjs +1084 -35
  31. package/dist/provision.mjs.map +1 -1
  32. package/dist/provision_activate.d.ts +146 -0
  33. package/dist/provision_activate.d.ts.map +1 -0
  34. package/dist/{tus_client-573066b3.mjs → tus_client-b1c35e8c.mjs} +2 -2
  35. package/dist/{tus_client-573066b3.mjs.map → tus_client-b1c35e8c.mjs.map} +1 -1
  36. package/dist/{tus_client-83ad0f4f.js → tus_client-d0798623.js} +2 -2
  37. package/dist/{tus_client-83ad0f4f.js.map → tus_client-d0798623.js.map} +1 -1
  38. package/package.json +1 -1
  39. package/dist/ndm_proxy-68354a89.js +0 -13
  40. package/dist/ndm_proxy-68354a89.js.map +0 -1
  41. package/dist/ndm_proxy-c00bc561.mjs.map +0 -1
  42. package/dist/ndn_types-859bbd48.js +0 -2
  43. package/dist/ndn_types-859bbd48.js.map +0 -1
  44. package/dist/ndn_types-f6c08d20.mjs.map +0 -1
@@ -36,7 +36,8 @@ type SourceKind = 'catalog' | 'pikg' | 'url'
36
36
  export interface PikgSnapshot {
37
37
  kind: 'pikg' | 'url'
38
38
  display: string
39
- bytes: Uint8Array
39
+ path: string
40
+ localFileId?: string
40
41
  digest: string
41
42
  size: number
42
43
  }
@@ -516,7 +517,7 @@ async function installApp(
516
517
  ...sourceRpcParams(source),
517
518
  ...planScopeAndOptions(inspection.plan),
518
519
  options: installOptions(input),
519
- plan: submittedPlan ?? null,
520
+ plan: submittedPlan ?? inspection.plan,
520
521
  approved_plan_fingerprint: expectString(inspection.plan, 'plan_fingerprint'),
521
522
  idempotency_key: idempotencyKey(ctx),
522
523
  }
@@ -593,6 +594,7 @@ async function upgradeApps(
593
594
  ...sourceRpcParams(source),
594
595
  ...planScopeAndOptions(inspection.plan),
595
596
  options: installOptions(input),
597
+ plan: inspection.plan,
596
598
  approved_plan_fingerprint: expectString(inspection.plan, 'plan_fingerprint'),
597
599
  idempotency_key: idempotencyKey(ctx),
598
600
  }),
@@ -675,37 +677,78 @@ async function prepareSource(
675
677
  const classified = await classifySource(ctx, input)
676
678
  if (classified.kind === 'catalog') return classified
677
679
 
678
- const snapshot = classified.kind === 'url'
679
- ? await downloadPikg(classified.url, dependencies.download, ctx.signal)
680
- : await readPikg(classified.path, classified.kind)
681
- const stage = dependencies.stagePikg ?? stagePikg
682
- const metadata = await stage(ctx, snapshot, purpose)
683
- if (metadata.schema_version !== PLAN_SCHEMA_VERSION) {
684
- throw new ToolError('UNSUPPORTED_SCHEMA_VERSION', 'staging returned a non-v4 schema')
685
- }
686
- if (metadata.pikg_digest !== snapshot.digest || metadata.size !== snapshot.size) {
687
- throw new ToolError(
688
- 'PIKG_DIGEST_MISMATCH',
689
- 'the staged PIKG does not match the client byte snapshot',
690
- EXIT_OPERATION,
691
- false,
692
- {
693
- expected_digest: snapshot.digest,
694
- staged_digest: metadata.pikg_digest,
695
- expected_size: snapshot.size,
696
- staged_size: metadata.size,
697
- },
698
- )
680
+ const host = getHost()
681
+ const localDirectory = localPikgDirectory(ctx)
682
+ let directory: string
683
+ let localFileId: string | undefined
684
+ if (localDirectory) {
685
+ try {
686
+ await host.mkdir(localDirectory, { recursive: true, mode: 0o700 })
687
+ directory = localDirectory
688
+ localFileId = crypto.randomUUID().replaceAll('-', '')
689
+ } catch (error) {
690
+ if (!isHostError(error, 'PermissionDenied') && !isHostError(error, 'NotFound')) throw error
691
+ }
699
692
  }
700
- if (metadata.purpose !== purpose) {
701
- throw new ToolError('INVALID_SERVICE_RESPONSE', 'staging purpose does not match the request', 9)
693
+ if (!localFileId) {
694
+ await host.mkdir(ctx.configStore.root, { recursive: true, mode: 0o700 })
695
+ directory = await host.makeTempDir({ dir: ctx.configStore.root, prefix: '.pikg-' })
702
696
  }
703
- return {
704
- kind: snapshot.kind,
705
- display: snapshot.display,
706
- snapshot,
707
- staging: metadata,
708
- serviceSource: { kind: 'local_pikg', staging_handle: metadata.handle },
697
+ let path = host.path.join(directory!, `${localFileId ?? 'source'}.pikg`)
698
+ const snapshotAt = (destination: string) =>
699
+ classified.kind === 'url'
700
+ ? downloadPikg(classified.url, destination, dependencies.download, ctx.signal)
701
+ : readPikg(classified.path, destination, ctx.signal)
702
+ try {
703
+ let snapshot: PikgSnapshot
704
+ try {
705
+ snapshot = await snapshotAt(path)
706
+ } catch (error) {
707
+ if (!localFileId || !isHostError(error, 'PermissionDenied')) throw error
708
+ await host.remove(path).catch(() => {})
709
+ localFileId = undefined
710
+ await host.mkdir(ctx.configStore.root, { recursive: true, mode: 0o700 })
711
+ directory = await host.makeTempDir({ dir: ctx.configStore.root, prefix: '.pikg-' })
712
+ path = host.path.join(directory, 'source.pikg')
713
+ snapshot = await snapshotAt(path)
714
+ }
715
+ snapshot.localFileId = localFileId
716
+ const stage = dependencies.stagePikg ?? stagePikg
717
+ const metadata = await stage(ctx, snapshot, purpose)
718
+ if (metadata.schema_version !== PLAN_SCHEMA_VERSION) {
719
+ throw new ToolError('UNSUPPORTED_SCHEMA_VERSION', 'staging returned a non-v4 schema')
720
+ }
721
+ if (metadata.pikg_digest !== snapshot.digest || metadata.size !== snapshot.size) {
722
+ throw new ToolError(
723
+ 'PIKG_DIGEST_MISMATCH',
724
+ 'the staged PIKG does not match the client byte snapshot',
725
+ EXIT_OPERATION,
726
+ false,
727
+ {
728
+ expected_digest: snapshot.digest,
729
+ staged_digest: metadata.pikg_digest,
730
+ expected_size: snapshot.size,
731
+ staged_size: metadata.size,
732
+ },
733
+ )
734
+ }
735
+ if (metadata.purpose !== purpose) {
736
+ throw new ToolError(
737
+ 'INVALID_SERVICE_RESPONSE',
738
+ 'staging purpose does not match the request',
739
+ 9,
740
+ )
741
+ }
742
+ return {
743
+ kind: snapshot.kind,
744
+ display: snapshot.display,
745
+ snapshot,
746
+ staging: metadata,
747
+ serviceSource: { kind: 'local_pikg', staging_handle: metadata.handle },
748
+ }
749
+ } finally {
750
+ await host.remove(localFileId ? path : directory!, { recursive: !localFileId })
751
+ .catch(() => {})
709
752
  }
710
753
  }
711
754
 
@@ -760,37 +803,82 @@ async function classifySource(
760
803
  }
761
804
  }
762
805
 
763
- async function readPikg(path: string, kind: 'pikg'): Promise<PikgSnapshot> {
806
+ function localPikgDirectory(ctx: CommandContext): string | undefined {
807
+ const root = getHost().policy.buckyosRoot
808
+ const hostname = new URL(ctx.connection.endpoint).hostname
809
+ if (!root || !['localhost', '127.0.0.1', '[::1]'].includes(hostname)) return undefined
810
+ return getHost().path.join(root, 'cache', 'control_panel', 'pikg_staging', 'incoming')
811
+ }
812
+
813
+ async function* fileChunks(file: HostFile, size: number): AsyncGenerator<Uint8Array> {
814
+ const buffer = new Uint8Array(size)
815
+ while (true) {
816
+ let used = 0
817
+ while (used < buffer.length) {
818
+ const read = await file.read(buffer.subarray(used))
819
+ if (read === null) break
820
+ if (read === 0) throw new UsageError('PIKG_READ_FAILED', 'PIKG read made no progress')
821
+ used += read
822
+ }
823
+ if (used === 0) break
824
+ yield buffer.subarray(0, used)
825
+ }
826
+ }
827
+
828
+ async function writePikgSnapshot(
829
+ path: string,
830
+ chunks: AsyncIterable<Uint8Array> | Iterable<Uint8Array>,
831
+ signal: AbortSignal,
832
+ ): Promise<{ path: string; digest: string; size: number }> {
833
+ const output = await getHost().open(path, { write: true, createNew: true, mode: 0o600 })
834
+ const hash = getHost().createHash('sha256')
835
+ const magic = new Uint8Array(ZIP_LOCAL_MAGIC.length)
836
+ let size = 0
837
+ try {
838
+ for await (const chunk of chunks) {
839
+ signal.throwIfAborted()
840
+ if (size < magic.length) magic.set(chunk.subarray(0, magic.length - size), size)
841
+ hash.update(chunk)
842
+ size += chunk.length
843
+ if (!Number.isSafeInteger(size)) throw new UsageError('PIKG_TOO_LARGE', 'PIKG is too large')
844
+ let offset = 0
845
+ while (offset < chunk.length) {
846
+ const written = await output.write(chunk.subarray(offset))
847
+ if (written === 0) throw new UsageError('PIKG_WRITE_FAILED', 'PIKG write made no progress')
848
+ offset += written
849
+ }
850
+ }
851
+ validatePikgSnapshot(magic.subarray(0, Math.min(size, magic.length)))
852
+ await output.sync()
853
+ return { path, size, digest: hash.digestHex() }
854
+ } finally {
855
+ await output.close()
856
+ }
857
+ }
858
+
859
+ async function readPikg(
860
+ source: string,
861
+ destination: string,
862
+ signal: AbortSignal,
863
+ ): Promise<PikgSnapshot> {
764
864
  let file: HostFile
765
865
  try {
766
- file = await getHost().open(path, { read: true })
866
+ file = await getHost().open(source, { read: true })
767
867
  } catch (error) {
768
868
  throw new UsageError('PIKG_READ_FAILED', `failed to open PIKG: ${errorMessage(error)}`)
769
869
  }
770
870
  try {
771
- const stat = await file.stat()
772
- if (!stat.isFile) throw new UsageError('INVALID_PIKG_SOURCE', 'PIKG source is not a file')
773
- if (stat.size > Number.MAX_SAFE_INTEGER) {
774
- throw new UsageError('PIKG_TOO_LARGE', 'PIKG is too large for this client')
775
- }
776
- const bytes = new Uint8Array(stat.size)
777
- let offset = 0
778
- while (offset < bytes.length) {
779
- const read = await file.read(bytes.subarray(offset))
780
- if (read === null) break
781
- offset += read
782
- }
783
- if (offset !== bytes.length) {
784
- throw new UsageError('PIKG_READ_FAILED', 'PIKG changed size while being read')
785
- }
786
- validatePikgSnapshot(bytes)
787
- return {
788
- kind,
789
- display: path,
790
- bytes,
791
- digest: await sha256Id(bytes),
792
- size: bytes.length,
871
+ const before = await file.stat()
872
+ if (!before.isFile) throw new UsageError('INVALID_PIKG_SOURCE', 'PIKG source is not a file')
873
+ const snapshot = await writePikgSnapshot(destination, fileChunks(file, 256 * 1024), signal)
874
+ const after = await file.stat()
875
+ if (
876
+ snapshot.size !== before.size || before.size !== after.size ||
877
+ before.mtime?.getTime() !== after.mtime?.getTime()
878
+ ) {
879
+ throw new UsageError('PIKG_READ_FAILED', 'PIKG changed while being read')
793
880
  }
881
+ return { kind: 'pikg', display: source, ...snapshot }
794
882
  } finally {
795
883
  await file.close()
796
884
  }
@@ -798,34 +886,33 @@ async function readPikg(path: string, kind: 'pikg'): Promise<PikgSnapshot> {
798
886
 
799
887
  async function downloadPikg(
800
888
  url: URL,
889
+ destination: string,
801
890
  downloader: AppModuleDependencies['download'],
802
891
  signal: AbortSignal,
803
892
  ): Promise<PikgSnapshot> {
804
- let bytes: Uint8Array
893
+ let chunks: Uint8Array[] | ReadableStream<Uint8Array> | undefined
805
894
  try {
806
- bytes = downloader ? await downloader(url, signal) : await defaultDownload(url, signal)
895
+ chunks = downloader ? [await downloader(url, signal)] : await defaultDownload(url, signal)
896
+ const snapshot = await writePikgSnapshot(destination, chunks, signal)
897
+ return { kind: 'url', display: safeUrl(url), ...snapshot }
807
898
  } catch (error) {
808
- if (error instanceof ToolError) throw error
899
+ if (error instanceof ToolError || isHostError(error, 'PermissionDenied')) throw error
900
+ signal.throwIfAborted()
809
901
  throw new ToolError(
810
902
  'PIKG_DOWNLOAD_FAILED',
811
903
  `failed to download PIKG: ${errorMessage(error)}`,
812
904
  5,
813
905
  true,
814
906
  )
815
- }
816
- validatePikgSnapshot(bytes)
817
- return {
818
- kind: 'url',
819
- display: safeUrl(url),
820
- bytes,
821
- digest: await sha256Id(bytes),
822
- size: bytes.length,
907
+ } finally {
908
+ if (chunks instanceof ReadableStream && !chunks.locked) await chunks.cancel().catch(() => {})
823
909
  }
824
910
  }
825
911
 
826
- async function defaultDownload(url: URL, signal: AbortSignal): Promise<Uint8Array> {
912
+ async function defaultDownload(url: URL, signal: AbortSignal): Promise<ReadableStream<Uint8Array>> {
827
913
  const response = await fetch(url, { method: 'GET', redirect: 'follow', signal })
828
- if (!response.ok) {
914
+ if (!response.ok || !response.body) {
915
+ await response.body?.cancel()
829
916
  throw new ToolError(
830
917
  'PIKG_DOWNLOAD_FAILED',
831
918
  `PIKG download returned HTTP ${response.status}`,
@@ -833,7 +920,7 @@ async function defaultDownload(url: URL, signal: AbortSignal): Promise<Uint8Arra
833
920
  response.status >= 500,
834
921
  )
835
922
  }
836
- return new Uint8Array(await response.arrayBuffer())
923
+ return response.body
837
924
  }
838
925
 
839
926
  async function stagePikg(
@@ -841,42 +928,80 @@ async function stagePikg(
841
928
  snapshot: PikgSnapshot,
842
929
  purpose: PikgPurpose,
843
930
  ): Promise<PikgStagingMetadata> {
931
+ const identity = { pikg_digest: snapshot.digest, size: snapshot.size, purpose }
932
+ if (snapshot.localFileId) {
933
+ try {
934
+ const value = expectObject(
935
+ await callControl(ctx, 'apps.staging.finalize', {
936
+ ...identity,
937
+ local_file_id: snapshot.localFileId,
938
+ }),
939
+ 'apps.staging.finalize response',
940
+ )
941
+ return parseStagingMetadata(value)
942
+ } catch (error) {
943
+ if (!errorMessage(error).includes('LOCAL_PIKG_UNAVAILABLE')) throw error
944
+ }
945
+ }
844
946
  const session = ctx.session
845
947
  if (!session) throw new ToolError('AUTH_REQUIRED', 'authenticated session is required', 3)
846
948
  const token = (await session.ensureValid()).token
847
- const hash = ndn.sha256Bytes(snapshot.bytes)
848
- const chunkId = ndn.ChunkId.fromMix256Result(snapshot.size, hash).toString()
849
949
  const proxy = ndm_proxy.createNdmProxyClient({
850
950
  endpoint: gatewayOrigin(ctx.connection.endpoint),
851
951
  sessionToken: token,
852
952
  fetcher: (request: RequestInfo | URL, init?: RequestInit) =>
853
953
  fetch(normalizeNdmRequestUrl(request), { ...init, signal: ctx.signal }),
854
954
  })
855
- let uploadError: unknown
955
+ let fileObjectId: string
856
956
  try {
857
- await proxy.putChunk(chunkId, snapshot.bytes)
858
- } catch (error) {
859
- uploadError = error
860
- }
861
- let value: Record<string, unknown>
862
- try {
863
- value = expectObject(
864
- await callControl(ctx, 'apps.staging.finalize', {
865
- source_obj_id: chunkId,
866
- purpose,
867
- }),
868
- 'apps.staging.finalize response',
869
- )
957
+ const file = await getHost().open(snapshot.path, { read: true })
958
+ const chunks = new ndn.SimpleChunkList()
959
+ const hash = getHost().createHash('sha256')
960
+ try {
961
+ for await (const bytes of fileChunks(file, 32 * 1024 * 1024)) {
962
+ ctx.signal.throwIfAborted()
963
+ hash.update(bytes)
964
+ const chunk = ndn.ChunkId.fromMix256Result(bytes.length, ndn.sha256Bytes(bytes))
965
+ await proxy.putChunk(chunk.toString(), bytes)
966
+ chunks.appendChunk(chunk)
967
+ }
968
+ } finally {
969
+ await file.close()
970
+ }
971
+ if (chunks.total_size !== snapshot.size || hash.digestHex() !== snapshot.digest) {
972
+ throw new ToolError('PIKG_DIGEST_MISMATCH', 'PIKG snapshot changed during upload')
973
+ }
974
+ let content = chunks.body[0].toString()
975
+ if (chunks.body.length > 1) {
976
+ const [id, body] = chunks.genObjId()
977
+ await proxy.putObject({ obj_id: id.toString(), obj_data: body })
978
+ content = id.toString()
979
+ }
980
+ const fileObject = new ndn.FileObject(`${snapshot.digest}.pikg`, snapshot.size, content)
981
+ const [id, body] = fileObject.genObjId()
982
+ await proxy.putObject({ obj_id: id.toString(), obj_data: body })
983
+ fileObjectId = id.toString()
870
984
  } catch (error) {
871
- if (uploadError === undefined) throw error
985
+ if (error instanceof ToolError) throw error
986
+ ctx.signal.throwIfAborted()
872
987
  throw new ToolError(
873
988
  'PIKG_UPLOAD_FAILED',
874
- `failed to upload PIKG snapshot: ${errorMessage(uploadError)}`,
989
+ `failed to upload PIKG: ${errorMessage(error)}`,
875
990
  5,
876
991
  true,
877
- { finalize_error: errorMessage(error) },
878
992
  )
879
993
  }
994
+ const value = expectObject(
995
+ await callControl(ctx, 'apps.staging.finalize', {
996
+ ...identity,
997
+ source_obj_id: fileObjectId,
998
+ }),
999
+ 'apps.staging.finalize response',
1000
+ )
1001
+ return parseStagingMetadata(value)
1002
+ }
1003
+
1004
+ function parseStagingMetadata(value: Record<string, unknown>): PikgStagingMetadata {
880
1005
  return {
881
1006
  schema_version: expectNumber(value, 'schema_version'),
882
1007
  handle: expectString(value, 'handle'),
@@ -1445,12 +1570,6 @@ function validatePikgSnapshot(bytes: Uint8Array): void {
1445
1570
  }
1446
1571
  }
1447
1572
 
1448
- async function sha256Id(bytes: Uint8Array): Promise<string> {
1449
- const digestInput = Uint8Array.from(bytes)
1450
- const digest = new Uint8Array(await crypto.subtle.digest('SHA-256', digestInput.buffer))
1451
- return [...digest].map((byte) => byte.toString(16).padStart(2, '0')).join('')
1452
- }
1453
-
1454
1573
  function gatewayOrigin(endpoint: string): string {
1455
1574
  const url = new URL(endpoint)
1456
1575
  const marker = url.pathname.indexOf('/kapi')