buckyos 0.7.127 → 0.7.128

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 (41) hide show
  1. package/cli/dist/cli.mjs +116 -31
  2. package/cli/dist/cli.mjs.map +1 -1
  3. package/cli/launcher.mjs +0 -0
  4. package/cli/modules/pikg.ts +5 -0
  5. package/cli/modules/pikg_protocol.ts +40 -0
  6. package/cli/runtime/host_deno.ts +16 -31
  7. package/cli/runtime/host_node.ts +15 -30
  8. package/cli/runtime/path_policy.ts +49 -0
  9. package/cli/version.ts +1 -1
  10. package/dist/app-frame.cjs +2 -0
  11. package/dist/app-frame.cjs.map +1 -0
  12. package/dist/app-frame.d.ts +82 -0
  13. package/dist/app-frame.d.ts.map +1 -0
  14. package/dist/app-frame.mjs +189 -0
  15. package/dist/app-frame.mjs.map +1 -0
  16. package/dist/browser.cjs +1 -1
  17. package/dist/browser.mjs +2 -2
  18. package/dist/content.cjs +2 -0
  19. package/dist/content.cjs.map +1 -0
  20. package/dist/content.d.ts +148 -0
  21. package/dist/content.d.ts.map +1 -0
  22. package/dist/content.mjs +361 -0
  23. package/dist/content.mjs.map +1 -0
  24. package/dist/content_mime.d.ts +4 -0
  25. package/dist/content_mime.d.ts.map +1 -0
  26. package/dist/index.cjs +1 -1
  27. package/dist/index.mjs +2 -2
  28. package/dist/{ndm_proxy-1b07ecf3.mjs → ndm_proxy-3caceb0a.mjs} +6 -2
  29. package/dist/{ndm_proxy-1b07ecf3.mjs.map → ndm_proxy-3caceb0a.mjs.map} +1 -1
  30. package/dist/{ndm_proxy-7b71deb4.js → ndm_proxy-d37a0204.js} +2 -2
  31. package/dist/{ndm_proxy-7b71deb4.js.map → ndm_proxy-d37a0204.js.map} +1 -1
  32. package/dist/nfsp.cjs +2 -0
  33. package/dist/nfsp.cjs.map +1 -0
  34. package/dist/nfsp.d.ts +484 -0
  35. package/dist/nfsp.d.ts.map +1 -0
  36. package/dist/nfsp.mjs +466 -0
  37. package/dist/nfsp.mjs.map +1 -0
  38. package/dist/node.cjs +1 -1
  39. package/dist/node.mjs +2 -2
  40. package/dist/runtime.d.ts.map +1 -1
  41. package/package.json +18 -2
package/cli/launcher.mjs CHANGED
File without changes
@@ -23,6 +23,7 @@ import {
23
23
  rejectUnknown,
24
24
  stableJsonDigest,
25
25
  validatePermissions,
26
+ validateContentHandlers,
26
27
  validateServiceConfigTips,
27
28
  validateSnapshot,
28
29
  validateSubpackageName,
@@ -80,6 +81,7 @@ interface AppMeta {
80
81
  show_name: string
81
82
  categories: string[]
82
83
  permissions: unknown[]
84
+ content_handlers?: unknown[]
83
85
  selector_type: string
84
86
  service_config_tips: Record<string, unknown>
85
87
  }
@@ -710,6 +712,7 @@ async function buildCommand(
710
712
  pkg_list: pkgList,
711
713
  show_name: appMeta.show_name,
712
714
  ...(appMeta.permissions.length ? { permissions: appMeta.permissions } : {}),
715
+ ...(appMeta.content_handlers?.length ? { content_handlers: appMeta.content_handlers } : {}),
713
716
  selector_type: appMeta.selector_type,
714
717
  service_config_tips: appMeta.service_config_tips,
715
718
  }
@@ -885,6 +888,7 @@ function parseAppMeta(value: Record<string, unknown>): AppMeta {
885
888
  'show_name',
886
889
  'categories',
887
890
  'permissions',
891
+ 'content_handlers',
888
892
  'selector_type',
889
893
  'service_config_tips',
890
894
  ],
@@ -929,6 +933,7 @@ function parseAppMeta(value: Record<string, unknown>): AppMeta {
929
933
  show_name: developmentString(value.show_name, 'app.json.show_name'),
930
934
  categories: categories as string[],
931
935
  permissions,
936
+ ...(value.content_handlers === undefined ? {} : { content_handlers: developmentValidation(() => validateContentHandlers(value.content_handlers, permissions)) }),
932
937
  selector_type: developmentString(value.selector_type, 'app.json.selector_type'),
933
938
  service_config_tips: serviceConfig,
934
939
  }
@@ -828,6 +828,7 @@ export function validateAppDocShape(appDoc: Record<string, unknown>): void {
828
828
  'sdk_version',
829
829
  'req_capbilities',
830
830
  'permissions',
831
+ 'content_handlers',
831
832
  'selector_type',
832
833
  'service_config_tips',
833
834
  ],
@@ -897,6 +898,9 @@ export function validateAppDocShape(appDoc: Record<string, unknown>): void {
897
898
  throw invalid('appdoc', 'APPDOC.permissions must be an array')
898
899
  }
899
900
  validatePermissions(appDoc.permissions ?? [], 'APPDOC.permissions')
901
+ if (appDoc.content_handlers !== undefined && !Array.isArray(appDoc.content_handlers)) {
902
+ throw invalid('appdoc', 'APPDOC.content_handlers must be an array')
903
+ }
900
904
  validateServiceConfigTips(appDoc.service_config_tips, 'APPDOC.service_config_tips')
901
905
  const pkgList = expectObject(appDoc.pkg_list, 'APPDOC.pkg_list')
902
906
  if (Object.keys(pkgList).length === 0) {
@@ -1130,3 +1134,39 @@ function invalid(stage: string, message: string, entry?: string): ToolError {
1130
1134
  export function stableJsonDigest(value: unknown): string {
1131
1135
  return sha256Bytes(new TextEncoder().encode(ndn.toCanonicalJsonString(value)))
1132
1136
  }
1137
+
1138
+ export function validateContentHandlers(value: unknown, permissions: unknown[]): unknown[] {
1139
+ if (!Array.isArray(value)) throw invalid('appdoc', 'content_handlers must be an array')
1140
+ const ids = new Set<string>()
1141
+ for (const raw of value) {
1142
+ const h = expectObject(raw, 'content handler')
1143
+ const id = expectNonEmptyString(h.handler_id, 'handler_id')
1144
+ if (!/^[a-zA-Z0-9_-]+$/.test(id) || ids.has(id)) throw invalid('appdoc', 'invalid or duplicate handler_id')
1145
+ ids.add(id)
1146
+ if (!Number.isSafeInteger(h.version) || Number(h.version) < 1) throw invalid('appdoc', 'invalid handler version')
1147
+ if (!Array.isArray(h.selectors) || !h.selectors.length) throw invalid('appdoc', 'missing selectors')
1148
+ for (const rawSelector of h.selectors) {
1149
+ const s = expectObject(rawSelector, 'selector')
1150
+ if (!['mime', 'objType', 'schema', 'ext'].some(k => k in s)) throw invalid('appdoc', 'empty selector')
1151
+ for (const key of ['mime', 'objType', 'schema']) {
1152
+ const v = s[key]
1153
+ if (v !== undefined && !(typeof v === 'string' && v.length) && !(Array.isArray(v) && v.length && v.every(i => typeof i === 'string' && i.length))) throw invalid('appdoc', 'invalid selector field')
1154
+ }
1155
+ if (s.maxSize !== undefined && (!Number.isSafeInteger(s.maxSize) || Number(s.maxSize) < 0)) throw invalid('appdoc', 'invalid maxSize')
1156
+ }
1157
+ const intents = expectObject(h.intents, 'intents')
1158
+ if (Object.keys(intents).length !== 1 || !intents.open) throw invalid('appdoc', 'only open intent is supported')
1159
+ const open = expectObject(intents.open, 'open')
1160
+ const entry = expectObject(open.entry, 'entry')
1161
+ const path = expectNonEmptyString(entry.path, 'entry.path')
1162
+ let decoded: string
1163
+ try { decoded = decodeURIComponent(path) } catch { throw invalid('appdoc', 'invalid entry path') }
1164
+ if (entry.type !== 'web' || !decoded.startsWith('/') || decoded.startsWith('//') || decoded.includes('..') || decoded.includes('\\') || decoded.includes('://') || decoded.includes('#') || /[\r\n]/.test(decoded)) throw invalid('appdoc', 'unsafe Web entry')
1165
+ if (open.priority !== undefined && (!Number.isSafeInteger(open.priority) || Number(open.priority) < 0 || Number(open.priority) > 80)) throw invalid('appdoc', 'priority must be 0..80')
1166
+ if (open.window !== undefined && open.window !== 'new' && open.window !== 'reuse') throw invalid('appdoc', 'invalid window policy')
1167
+ if (open.multiSource !== undefined && typeof open.multiSource !== 'boolean') throw invalid('appdoc', 'invalid multiSource')
1168
+ if (open.modes !== undefined && (!Array.isArray(open.modes) || !open.modes.length || open.modes.some(m => m !== 'view' && m !== 'edit'))) throw invalid('appdoc', 'invalid modes')
1169
+ if (h.permissions !== undefined && (!Array.isArray(h.permissions) || h.permissions.some(scope => !permissions.some(p => p && typeof p === 'object' && (p as Record<string, unknown>).scope_path === scope)))) throw invalid('appdoc', 'handler permissions exceed App permissions')
1170
+ }
1171
+ return value
1172
+ }
@@ -7,6 +7,7 @@ import { fileURLToPath } from 'node:url'
7
7
  import { Readable, Writable } from 'node:stream'
8
8
  import { pipeline } from 'node:stream/promises'
9
9
  import { constants as zlibConstants, createGzip } from 'node:zlib'
10
+ import { assertPathAccess } from './path_policy.ts'
10
11
  import {
11
12
  type DistributionPolicy,
12
13
  HostError,
@@ -392,28 +393,20 @@ export class DenoHost implements ToolHost {
392
393
  operation: 'read' | 'write',
393
394
  resolveLinks = true,
394
395
  ): Promise<void> {
395
- const roots = operation === 'read' ? this.policy.readPaths : this.policy.writePaths
396
- const absolute = denoPath.resolve(candidate)
397
- if (!insideAny(absolute, roots)) {
398
- throw new HostError(
399
- 'PermissionDenied',
400
- `${operation} access is outside ${this.policy.name}: ${absolute}`,
401
- absolute,
402
- )
403
- }
404
- if (!resolveLinks) return
405
- try {
406
- const physical = await Deno.realPath(absolute)
407
- if (!insideAny(physical, roots)) {
408
- throw new HostError(
409
- 'PermissionDenied',
410
- `${operation} access escapes ${this.policy.name}: ${absolute}`,
411
- absolute,
412
- )
413
- }
414
- } catch (error) {
415
- if (!(error instanceof Deno.errors.NotFound)) throw error
416
- }
396
+ await assertPathAccess(
397
+ this.policy,
398
+ denoPath,
399
+ async (path) => {
400
+ try {
401
+ return await Deno.realPath(path)
402
+ } catch (error) {
403
+ throw translateDenoError(error, path)
404
+ }
405
+ },
406
+ candidate,
407
+ operation,
408
+ resolveLinks,
409
+ )
417
410
  }
418
411
  }
419
412
 
@@ -486,19 +479,11 @@ function fileInfo(value: Deno.FileInfo): HostFileInfo {
486
479
  }
487
480
  }
488
481
 
489
- function insideAny(candidate: string, roots: readonly string[]): boolean {
490
- return roots.some((root) => {
491
- const relative = denoPath.relative(denoPath.resolve(root), candidate)
492
- return relative === '' || relative !== '..' && !relative.startsWith(`..${denoPath.sep}`) &&
493
- !denoPath.isAbsolute(relative)
494
- })
495
- }
496
-
497
482
  function translateDenoError(error: unknown, path?: string): HostError {
498
483
  if (error instanceof HostError) return error
499
484
  const kind = error instanceof Deno.errors.NotFound
500
485
  ? 'NotFound'
501
- : error instanceof Deno.errors.PermissionDenied
486
+ : error instanceof Deno.errors.PermissionDenied || error instanceof Deno.errors.NotCapable
502
487
  ? 'PermissionDenied'
503
488
  : error instanceof Deno.errors.AlreadyExists
504
489
  ? 'AlreadyExists'
@@ -24,6 +24,7 @@ import { createInterface } from 'node:readline/promises'
24
24
  import { Readable } from 'node:stream'
25
25
  import { pipeline } from 'node:stream/promises'
26
26
  import { constants as zlibConstants, createGzip } from 'node:zlib'
27
+ import { assertPathAccess } from './path_policy.ts'
27
28
  import {
28
29
  type DistributionPolicy,
29
30
  HostError,
@@ -399,28 +400,20 @@ export class NodeHost implements ToolHost {
399
400
  operation: 'read' | 'write',
400
401
  resolveLinks = true,
401
402
  ): Promise<void> {
402
- const roots = operation === 'read' ? this.policy.readPaths : this.policy.writePaths
403
- const absolute = nodePath.resolve(candidate)
404
- if (!insideAny(absolute, roots)) {
405
- throw new HostError(
406
- 'PermissionDenied',
407
- `${operation} access is outside ${this.policy.name}: ${absolute}`,
408
- absolute,
409
- )
410
- }
411
- if (!resolveLinks) return
412
- try {
413
- const physical = await realpath(absolute)
414
- if (!insideAny(physical, roots)) {
415
- throw new HostError(
416
- 'PermissionDenied',
417
- `${operation} access escapes ${this.policy.name}: ${absolute}`,
418
- absolute,
419
- )
420
- }
421
- } catch (error) {
422
- if ((error as { code?: string }).code !== 'ENOENT') throw error
423
- }
403
+ await assertPathAccess(
404
+ this.policy,
405
+ nodePath,
406
+ async (path) => {
407
+ try {
408
+ return await realpath(path)
409
+ } catch (error) {
410
+ throw translateNodeError(error, path)
411
+ }
412
+ },
413
+ candidate,
414
+ operation,
415
+ resolveLinks,
416
+ )
424
417
  }
425
418
  }
426
419
 
@@ -508,14 +501,6 @@ function fileInfo(value: {
508
501
  }
509
502
  }
510
503
 
511
- function insideAny(candidate: string, roots: readonly string[]): boolean {
512
- return roots.some((root) => {
513
- const relative = nodePath.relative(nodePath.resolve(root), candidate)
514
- return relative === '' || relative !== '..' && !relative.startsWith(`..${nodePath.sep}`) &&
515
- !nodePath.isAbsolute(relative)
516
- })
517
- }
518
-
519
504
  async function capture(command: string, args: string[]) {
520
505
  return await new Promise<{
521
506
  success: boolean
@@ -0,0 +1,49 @@
1
+ import { type DistributionPolicy, HostError, type HostPath, isHostError } from './host.ts'
2
+
3
+ export async function assertPathAccess(
4
+ policy: DistributionPolicy,
5
+ path: HostPath,
6
+ realPath: (value: string) => Promise<string>,
7
+ candidate: string,
8
+ operation: 'read' | 'write',
9
+ resolveLinks = true,
10
+ ): Promise<void> {
11
+ const roots = operation === 'read' ? policy.readPaths : policy.writePaths
12
+ const absolute = path.resolve(candidate)
13
+ const physicalRoots: string[] = []
14
+ for (const root of roots) {
15
+ try {
16
+ physicalRoots.push(await realPath(path.resolve(root)))
17
+ } catch (error) {
18
+ if (!isHostError(error, 'NotFound') && !isHostError(error, 'PermissionDenied')) throw error
19
+ }
20
+ }
21
+ if (!insideAny(absolute, roots, path) && !insideAny(absolute, physicalRoots, path)) {
22
+ throw new HostError(
23
+ 'PermissionDenied',
24
+ `${operation} access is outside ${policy.name}: ${absolute}`,
25
+ absolute,
26
+ )
27
+ }
28
+ if (!resolveLinks) return
29
+ try {
30
+ const physical = await realPath(absolute)
31
+ if (!insideAny(physical, physicalRoots, path)) {
32
+ throw new HostError(
33
+ 'PermissionDenied',
34
+ `${operation} access escapes ${policy.name}: ${absolute}`,
35
+ absolute,
36
+ )
37
+ }
38
+ } catch (error) {
39
+ if (!isHostError(error, 'NotFound')) throw error
40
+ }
41
+ }
42
+
43
+ function insideAny(candidate: string, roots: readonly string[], path: HostPath): boolean {
44
+ return roots.some((root) => {
45
+ const relative = path.relative(path.resolve(root), candidate)
46
+ return relative === '' || relative !== '..' && !relative.startsWith(`..${path.sep}`) &&
47
+ !path.isAbsolute(relative)
48
+ })
49
+ }
package/cli/version.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  // Generated by scripts/generate-cli-version.mjs. Do not edit.
2
- export const PACKAGE_VERSION = '0.7.127'
2
+ export const PACKAGE_VERSION = '0.7.128'
3
3
  export const TOOL_VERSION = PACKAGE_VERSION
4
4
  export const SDK_VERSION = PACKAGE_VERSION
5
5
  export const PROTOCOL_VERSION = '1'
@@ -0,0 +1,2 @@
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./content.cjs"),t=(e,t,i={})=>({protocol:"buckyos.app-frame",version:1,type:e,payload:t,...i}),i=e=>{if(!e||"object"!=typeof e)return!1;const t=e;return"buckyos.app-frame"===t.protocol&&1===t.version&&"string"==typeof t.type&&!!t.payload&&"object"==typeof t.payload};exports.AppFrameClient=class{constructor(e){if(this.options=e,this.listener=e=>{this.receive(e)},this.origin=new URL(e.shellOrigin).origin,this.nonce=new URLSearchParams(window.location.hash.slice(1)).get("bfp"),window.addEventListener("message",this.listener),window.parent!==window&&this.nonce){const i=()=>window.parent.postMessage(t("frame.hello",{nonce:this.nonce,appCapabilities:[...e.onOpen?["open"]:[],...e.onBeforeClose?["beforeClose"]:[],...e.onBack?["back"]:[]]}),this.origin);i(),this.helloTimer=setInterval(i,250),this.helloDeadline=setTimeout((()=>clearInterval(this.helloTimer)),1500)}}async receive(s){var n,o,r,a,l,p,c,h,d,u,y,f,w,m;if(s.source!==window.parent||s.origin!==this.origin||!i(s.data))return;const g=s.data,v=g.payload;if("frame.init"===g.type){if(!this.nonce||v.nonce!==this.nonce||"string"!=typeof v.locale||!v.theme||"string"!=typeof v.windowId||v.launch&&!e.isOpenRequest(v.launch))return;return clearInterval(this.helloTimer),clearTimeout(this.helloDeadline),this.init=v,void(null==(o=(n=this.options).onInit)||o.call(n,this.init))}if(!this.init)return;const b=e=>window.parent.postMessage(t(g.type+"Result",e,{replyTo:g.id}),this.origin);try{"frame.open"===g.type&&e.isOpenRequest(v.request)&&b(await(null==(a=(r=this.options).onOpen)?void 0:a.call(r,v.request))??{accepted:!1}),"frame.beforeClose"===g.type&&b({decision:await(null==(p=(l=this.options).onBeforeClose)?void 0:p.call(l,v.reason))??"allow"}),"frame.back"===g.type&&b({handled:await(null==(h=(c=this.options).onBack)?void 0:h.call(c))??!1}),"frame.themeChanged"!==g.type||"dark"!==v.mode&&"light"!==v.mode||null==(u=(d=this.options).onTheme)||u.call(d,v),"frame.localeChanged"===g.type&&"string"==typeof v.locale&&(null==(f=(y=this.options).onLocale)||f.call(y,v.locale)),"frame.focusChanged"===g.type&&(null==(m=(w=this.options).onFocus)||m.call(w,!0===v.focused))}catch{"frame.open"===g.type&&b({accepted:!1,fallback:"preview"}),"frame.beforeClose"===g.type&&b({decision:"deny"})}}send(e,i){this.init&&window.parent.postMessage(t(e,i),this.origin)}setTitle(e,t=!1){this.send("window.setTitle",{title:e,dirty:t})}requestClose(){this.send("window.requestClose",{})}openContent(e,t="default",i=!1){this.send("content.open",{request:e,target:t,newWindow:i})}dispose(){clearInterval(this.helloTimer),clearTimeout(this.helloDeadline),window.removeEventListener("message",this.listener)}},exports.AppFrameHost=class{constructor(e){this.options=e,this.ready=!1,this.capabilities=[],this.pending=new Map,this.listener=e=>this.receive(e),this.opens=[],window.addEventListener("message",this.listener)}receive(t){var s,n,o,r,a,l,p,c;if(t.source!==this.options.target()||t.origin!==this.options.origin||!i(t.data))return;const h=t.data,d=h.payload;if("frame.hello"===h.type){if(d.nonce!==this.options.init.nonce||!Array.isArray(d.appCapabilities))return;return this.ready=!0,this.capabilities=d.appCapabilities.filter((e=>"string"==typeof e)),this.send("frame.init",this.options.init),void(null==(n=(s=this.options).onReady)||n.call(s))}if(this.ready)if(h.replyTo){const e=this.pending.get(h.replyTo);e&&h.type===e.type+"Result"&&(clearTimeout(e.timer),this.pending.delete(h.replyTo),e.resolve(h.payload))}else if("window.setTitle"===h.type&&"string"==typeof d.title&&(null==(r=(o=this.options).onTitle)||r.call(o,d.title.slice(0,256),!0===d.dirty)),"window.requestClose"===h.type&&(null==(l=(a=this.options).onRequestClose)||l.call(a)),"content.open"===h.type&&("default"===d.target||"preview"===d.target)){const t={...d.request,requestId:crypto.randomUUID()};if(!e.isOpenRequest(t))return;if(this.opens=this.opens.filter((e=>e>Date.now()-1e4)),this.opens.length>=5)return;this.opens.push(Date.now()),null==(c=(p=this.options).onContentOpen)||c.call(p,t,d.target,!0===d.newWindow)}}update(e){this.options.init={...this.options.init,...e}}send(e,i){var s;this.ready&&(null==(s=this.options.target())||s.postMessage(t(e,i),this.options.origin))}request(e,i,s=3e3){if(!this.ready)return Promise.reject(new Error("App frame is not ready"));const n=crypto.randomUUID();return new Promise(((o,r)=>{var a;const l=setTimeout((()=>{this.pending.delete(n),r(new Error("App frame timed out"))}),s);this.pending.set(n,{resolve:o,reject:r,timer:l,type:e}),null==(a=this.options.target())||a.postMessage(t(e,i,{id:n}),this.options.origin)}))}async open(e){return this.capabilities.includes("open")?await this.request("frame.open",{request:e}):{accepted:!1}}async beforeClose(e="user"){if(!this.ready||!this.capabilities.includes("beforeClose"))return"allow";const t=await this.request("frame.beforeClose",{reason:e});return"allow"===t.decision||"pending"===t.decision?t.decision:"deny"}async back(){return!!this.capabilities.includes("back")&&!0===(await this.request("frame.back",{})).handled}dispose(){window.removeEventListener("message",this.listener),this.pending.forEach((e=>{clearTimeout(e.timer),e.reject(new Error("App frame disposed"))})),this.pending.clear(),this.ready=!1}};
2
+ //# sourceMappingURL=app-frame.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app-frame.cjs","sources":["../src/app-frame.ts"],"sourcesContent":["import { isOpenRequest, type OpenRequest } from './content'\nexport interface AppFrameMessage<T extends string = string, P = unknown> {\n protocol: 'buckyos.app-frame'; version: 1; type: T; id?: string; replyTo?: string; payload: P\n}\nexport interface FrameInit {\n nonce: string\n windowId: string\n formFactor: 'desktop' | 'mobile'\n theme: { mode: 'light' | 'dark'; accent?: string }\n locale: string\n launch?: OpenRequest\n shellCapabilities: string[]\n}\nexport type CloseDecision = 'allow' | 'deny' | 'pending'\nconst envelope = (type: string, payload: unknown, extra: { id?: string; replyTo?: string } = {}): AppFrameMessage =>\n ({ protocol: 'buckyos.app-frame', version: 1, type, payload, ...extra })\nconst isMessage = (value: unknown): value is AppFrameMessage => {\n if (!value || typeof value !== 'object') return false\n const v = value as AppFrameMessage\n return v.protocol === 'buckyos.app-frame' && v.version === 1 && typeof v.type === 'string' && !!v.payload && typeof v.payload === 'object'\n}\nexport class AppFrameHost {\n ready = false\n capabilities: string[] = []\n private pending = new Map<string, { resolve: (v: unknown) => void; reject: (e: Error) => void; timer: ReturnType<typeof setTimeout>; type: string }>()\n private listener = (event: MessageEvent) => this.receive(event)\n private opens: number[] = []\n constructor(private options: {\n target: () => Window | null; origin: string; init: FrameInit\n onReady?: () => void\n onTitle?: (title: string, dirty: boolean) => void\n onRequestClose?: () => void\n onContentOpen?: (request: OpenRequest, target: 'default' | 'preview', newWindow: boolean) => void\n }) { window.addEventListener('message', this.listener) }\n private receive(event: MessageEvent): void {\n if (event.source !== this.options.target() || event.origin !== this.options.origin || !isMessage(event.data)) return\n const m = event.data\n const p = m.payload as Record<string, unknown>\n if (m.type === 'frame.hello') {\n if (p.nonce !== this.options.init.nonce || !Array.isArray(p.appCapabilities)) return\n this.ready = true\n this.capabilities = p.appCapabilities.filter((c): c is string => typeof c === 'string')\n this.send('frame.init', this.options.init)\n this.options.onReady?.()\n return\n }\n if (!this.ready) return\n if (m.replyTo) {\n const pending = this.pending.get(m.replyTo)\n if (pending && m.type === pending.type + 'Result') {\n clearTimeout(pending.timer); this.pending.delete(m.replyTo); pending.resolve(m.payload)\n }\n return\n }\n if (m.type === 'window.setTitle' && typeof p.title === 'string') this.options.onTitle?.(p.title.slice(0, 256), p.dirty === true)\n if (m.type === 'window.requestClose') this.options.onRequestClose?.()\n if (m.type === 'content.open' && (p.target === 'default' || p.target === 'preview')) {\n const request = { ...(p.request as object), requestId: crypto.randomUUID() }\n if (!isOpenRequest(request)) return\n this.opens = this.opens.filter(t => t > Date.now() - 10000)\n if (this.opens.length >= 5) return\n this.opens.push(Date.now())\n this.options.onContentOpen?.(request, p.target, p.newWindow === true)\n }\n }\n update(init: Partial<FrameInit>): void { this.options.init = { ...this.options.init, ...init } }\n send(type: string, payload: unknown): void {\n if (this.ready) this.options.target()?.postMessage(envelope(type, payload), this.options.origin)\n }\n request(type: string, payload: unknown, timeout = 3000): Promise<unknown> {\n if (!this.ready) return Promise.reject(new Error('App frame is not ready'))\n const id = crypto.randomUUID()\n return new Promise((resolve, reject) => {\n const timer = setTimeout(() => { this.pending.delete(id); reject(new Error('App frame timed out')) }, timeout)\n this.pending.set(id, { resolve, reject, timer, type })\n this.options.target()?.postMessage(envelope(type, payload, { id }), this.options.origin)\n })\n }\n async open(request: OpenRequest): Promise<{ accepted: boolean; fallback?: 'preview' }> {\n if (!this.capabilities.includes('open')) return { accepted: false }\n return await this.request('frame.open', { request }) as { accepted: boolean; fallback?: 'preview' }\n }\n async beforeClose(reason: 'user' | 'logout' | 'shell' = 'user'): Promise<CloseDecision> {\n if (!this.ready || !this.capabilities.includes('beforeClose')) return 'allow'\n const r = await this.request('frame.beforeClose', { reason }) as { decision?: CloseDecision }\n return r.decision === 'allow' || r.decision === 'pending' ? r.decision : 'deny'\n }\n async back(): Promise<boolean> {\n if (!this.capabilities.includes('back')) return false\n return (await this.request('frame.back', {}) as { handled?: boolean }).handled === true\n }\n dispose(): void {\n window.removeEventListener('message', this.listener)\n this.pending.forEach(p => { clearTimeout(p.timer); p.reject(new Error('App frame disposed')) })\n this.pending.clear(); this.ready = false\n }\n}\nexport class AppFrameClient {\n init?: FrameInit\n private helloTimer?: ReturnType<typeof setInterval>\n private helloDeadline?: ReturnType<typeof setTimeout>\n private readonly nonce: string | null\n private readonly origin: string\n private readonly listener = (event: MessageEvent) => { void this.receive(event) }\n constructor(private options: {\n shellOrigin: string\n onInit?: (init: FrameInit) => void\n onOpen?: (request: OpenRequest) => Promise<{ accepted: boolean; fallback?: 'preview' }> | { accepted: boolean; fallback?: 'preview' }\n onBeforeClose?: (reason: 'user' | 'logout' | 'shell') => Promise<CloseDecision> | CloseDecision\n onBack?: () => Promise<boolean> | boolean\n onTheme?: (theme: FrameInit['theme']) => void\n onLocale?: (locale: string) => void\n onFocus?: (focused: boolean) => void\n }) {\n this.origin = new URL(options.shellOrigin).origin\n this.nonce = new URLSearchParams(window.location.hash.slice(1)).get('bfp')\n window.addEventListener('message', this.listener)\n if (window.parent !== window && this.nonce) {\n const hello = () => window.parent.postMessage(envelope('frame.hello', { nonce: this.nonce, appCapabilities: [\n ...options.onOpen ? ['open'] : [], ...options.onBeforeClose ? ['beforeClose'] : [], ...options.onBack ? ['back'] : [],\n ] }), this.origin)\n hello()\n this.helloTimer = setInterval(hello, 250)\n this.helloDeadline = setTimeout(() => clearInterval(this.helloTimer), 1500)\n }\n }\n private async receive(event: MessageEvent): Promise<void> {\n if (event.source !== window.parent || event.origin !== this.origin || !isMessage(event.data)) return\n const m = event.data\n const p = m.payload as Record<string, unknown>\n if (m.type === 'frame.init') {\n if (!this.nonce || p.nonce !== this.nonce || typeof p.locale !== 'string' || !p.theme || typeof p.windowId !== 'string' || p.launch && !isOpenRequest(p.launch)) return\n clearInterval(this.helloTimer); clearTimeout(this.helloDeadline)\n this.init = p as unknown as FrameInit\n this.options.onInit?.(this.init)\n return\n }\n if (!this.init) return\n const reply = (payload: unknown) => window.parent.postMessage(envelope(m.type + 'Result', payload, { replyTo: m.id }), this.origin)\n try {\n if (m.type === 'frame.open' && isOpenRequest(p.request)) reply(await this.options.onOpen?.(p.request) ?? { accepted: false })\n if (m.type === 'frame.beforeClose') reply({ decision: await this.options.onBeforeClose?.(p.reason as 'user') ?? 'allow' })\n if (m.type === 'frame.back') reply({ handled: await this.options.onBack?.() ?? false })\n if (m.type === 'frame.themeChanged' && (p.mode === 'dark' || p.mode === 'light')) this.options.onTheme?.(p as FrameInit['theme'])\n if (m.type === 'frame.localeChanged' && typeof p.locale === 'string') this.options.onLocale?.(p.locale)\n if (m.type === 'frame.focusChanged') this.options.onFocus?.(p.focused === true)\n } catch {\n if (m.type === 'frame.open') reply({ accepted: false, fallback: 'preview' })\n if (m.type === 'frame.beforeClose') reply({ decision: 'deny' })\n }\n }\n send(type: string, payload: unknown): void {\n if (this.init) window.parent.postMessage(envelope(type, payload), this.origin)\n }\n setTitle(title: string, dirty = false): void { this.send('window.setTitle', { title, dirty }) }\n requestClose(): void { this.send('window.requestClose', {}) }\n openContent(request: Omit<OpenRequest, 'requestId'>, target: 'default' | 'preview' = 'default', newWindow = false): void {\n this.send('content.open', { request, target, newWindow })\n }\n dispose(): void { clearInterval(this.helloTimer); clearTimeout(this.helloDeadline); window.removeEventListener('message', this.listener) }\n}\n"],"names":["envelope","type","payload","extra","protocol","version","isMessage","value","v","constructor","options","this","listener","event","receive","origin","URL","shellOrigin","nonce","URLSearchParams","window","location","hash","slice","get","addEventListener","parent","hello","postMessage","appCapabilities","onOpen","onBeforeClose","onBack","helloTimer","setInterval","helloDeadline","setTimeout","clearInterval","source","data","m","p","locale","theme","windowId","launch","isOpenRequest","clearTimeout","init","_b","_a","onInit","call","reply","replyTo","id","request","_d","_c","accepted","decision","_f","_e","reason","handled","_h","_g","mode","_j","_i","onTheme","_l","_k","onLocale","_n","_m","onFocus","focused","fallback","send","setTitle","title","dirty","requestClose","openContent","target","newWindow","dispose","removeEventListener","ready","capabilities","pending","Map","opens","Array","isArray","filter","c","onReady","timer","delete","resolve","onTitle","onRequestClose","requestId","crypto","randomUUID","t","Date","now","length","push","onContentOpen","update","timeout","Promise","reject","Error","set","open","includes","beforeClose","r","back","forEach","clear"],"mappings":"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"}
@@ -0,0 +1,82 @@
1
+ import { type OpenRequest } from './content';
2
+ export interface AppFrameMessage<T extends string = string, P = unknown> {
3
+ protocol: 'buckyos.app-frame';
4
+ version: 1;
5
+ type: T;
6
+ id?: string;
7
+ replyTo?: string;
8
+ payload: P;
9
+ }
10
+ export interface FrameInit {
11
+ nonce: string;
12
+ windowId: string;
13
+ formFactor: 'desktop' | 'mobile';
14
+ theme: {
15
+ mode: 'light' | 'dark';
16
+ accent?: string;
17
+ };
18
+ locale: string;
19
+ launch?: OpenRequest;
20
+ shellCapabilities: string[];
21
+ }
22
+ export type CloseDecision = 'allow' | 'deny' | 'pending';
23
+ export declare class AppFrameHost {
24
+ private options;
25
+ ready: boolean;
26
+ capabilities: string[];
27
+ private pending;
28
+ private listener;
29
+ private opens;
30
+ constructor(options: {
31
+ target: () => Window | null;
32
+ origin: string;
33
+ init: FrameInit;
34
+ onReady?: () => void;
35
+ onTitle?: (title: string, dirty: boolean) => void;
36
+ onRequestClose?: () => void;
37
+ onContentOpen?: (request: OpenRequest, target: 'default' | 'preview', newWindow: boolean) => void;
38
+ });
39
+ private receive;
40
+ update(init: Partial<FrameInit>): void;
41
+ send(type: string, payload: unknown): void;
42
+ request(type: string, payload: unknown, timeout?: number): Promise<unknown>;
43
+ open(request: OpenRequest): Promise<{
44
+ accepted: boolean;
45
+ fallback?: 'preview';
46
+ }>;
47
+ beforeClose(reason?: 'user' | 'logout' | 'shell'): Promise<CloseDecision>;
48
+ back(): Promise<boolean>;
49
+ dispose(): void;
50
+ }
51
+ export declare class AppFrameClient {
52
+ private options;
53
+ init?: FrameInit;
54
+ private helloTimer?;
55
+ private helloDeadline?;
56
+ private readonly nonce;
57
+ private readonly origin;
58
+ private readonly listener;
59
+ constructor(options: {
60
+ shellOrigin: string;
61
+ onInit?: (init: FrameInit) => void;
62
+ onOpen?: (request: OpenRequest) => Promise<{
63
+ accepted: boolean;
64
+ fallback?: 'preview';
65
+ }> | {
66
+ accepted: boolean;
67
+ fallback?: 'preview';
68
+ };
69
+ onBeforeClose?: (reason: 'user' | 'logout' | 'shell') => Promise<CloseDecision> | CloseDecision;
70
+ onBack?: () => Promise<boolean> | boolean;
71
+ onTheme?: (theme: FrameInit['theme']) => void;
72
+ onLocale?: (locale: string) => void;
73
+ onFocus?: (focused: boolean) => void;
74
+ });
75
+ private receive;
76
+ send(type: string, payload: unknown): void;
77
+ setTitle(title: string, dirty?: boolean): void;
78
+ requestClose(): void;
79
+ openContent(request: Omit<OpenRequest, 'requestId'>, target?: 'default' | 'preview', newWindow?: boolean): void;
80
+ dispose(): void;
81
+ }
82
+ //# sourceMappingURL=app-frame.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app-frame.d.ts","sourceRoot":"","sources":["../src/app-frame.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,189 @@
1
+ import { isOpenRequest } from "./content.mjs";
2
+ const envelope = (type, payload, extra = {}) => ({ protocol: "buckyos.app-frame", version: 1, type, payload, ...extra });
3
+ const isMessage = (value) => {
4
+ if (!value || typeof value !== "object")
5
+ return false;
6
+ const v = value;
7
+ return v.protocol === "buckyos.app-frame" && v.version === 1 && typeof v.type === "string" && !!v.payload && typeof v.payload === "object";
8
+ };
9
+ class AppFrameHost {
10
+ constructor(options) {
11
+ this.options = options;
12
+ this.ready = false;
13
+ this.capabilities = [];
14
+ this.pending = /* @__PURE__ */ new Map();
15
+ this.listener = (event) => this.receive(event);
16
+ this.opens = [];
17
+ window.addEventListener("message", this.listener);
18
+ }
19
+ receive(event) {
20
+ var _a, _b, _c, _d, _e, _f, _g, _h;
21
+ if (event.source !== this.options.target() || event.origin !== this.options.origin || !isMessage(event.data))
22
+ return;
23
+ const m = event.data;
24
+ const p = m.payload;
25
+ if (m.type === "frame.hello") {
26
+ if (p.nonce !== this.options.init.nonce || !Array.isArray(p.appCapabilities))
27
+ return;
28
+ this.ready = true;
29
+ this.capabilities = p.appCapabilities.filter((c) => typeof c === "string");
30
+ this.send("frame.init", this.options.init);
31
+ (_b = (_a = this.options).onReady) == null ? void 0 : _b.call(_a);
32
+ return;
33
+ }
34
+ if (!this.ready)
35
+ return;
36
+ if (m.replyTo) {
37
+ const pending = this.pending.get(m.replyTo);
38
+ if (pending && m.type === pending.type + "Result") {
39
+ clearTimeout(pending.timer);
40
+ this.pending.delete(m.replyTo);
41
+ pending.resolve(m.payload);
42
+ }
43
+ return;
44
+ }
45
+ if (m.type === "window.setTitle" && typeof p.title === "string")
46
+ (_d = (_c = this.options).onTitle) == null ? void 0 : _d.call(_c, p.title.slice(0, 256), p.dirty === true);
47
+ if (m.type === "window.requestClose")
48
+ (_f = (_e = this.options).onRequestClose) == null ? void 0 : _f.call(_e);
49
+ if (m.type === "content.open" && (p.target === "default" || p.target === "preview")) {
50
+ const request = { ...p.request, requestId: crypto.randomUUID() };
51
+ if (!isOpenRequest(request))
52
+ return;
53
+ this.opens = this.opens.filter((t) => t > Date.now() - 1e4);
54
+ if (this.opens.length >= 5)
55
+ return;
56
+ this.opens.push(Date.now());
57
+ (_h = (_g = this.options).onContentOpen) == null ? void 0 : _h.call(_g, request, p.target, p.newWindow === true);
58
+ }
59
+ }
60
+ update(init) {
61
+ this.options.init = { ...this.options.init, ...init };
62
+ }
63
+ send(type, payload) {
64
+ var _a;
65
+ if (this.ready)
66
+ (_a = this.options.target()) == null ? void 0 : _a.postMessage(envelope(type, payload), this.options.origin);
67
+ }
68
+ request(type, payload, timeout = 3e3) {
69
+ if (!this.ready)
70
+ return Promise.reject(new Error("App frame is not ready"));
71
+ const id = crypto.randomUUID();
72
+ return new Promise((resolve, reject) => {
73
+ var _a;
74
+ const timer = setTimeout(() => {
75
+ this.pending.delete(id);
76
+ reject(new Error("App frame timed out"));
77
+ }, timeout);
78
+ this.pending.set(id, { resolve, reject, timer, type });
79
+ (_a = this.options.target()) == null ? void 0 : _a.postMessage(envelope(type, payload, { id }), this.options.origin);
80
+ });
81
+ }
82
+ async open(request) {
83
+ if (!this.capabilities.includes("open"))
84
+ return { accepted: false };
85
+ return await this.request("frame.open", { request });
86
+ }
87
+ async beforeClose(reason = "user") {
88
+ if (!this.ready || !this.capabilities.includes("beforeClose"))
89
+ return "allow";
90
+ const r = await this.request("frame.beforeClose", { reason });
91
+ return r.decision === "allow" || r.decision === "pending" ? r.decision : "deny";
92
+ }
93
+ async back() {
94
+ if (!this.capabilities.includes("back"))
95
+ return false;
96
+ return (await this.request("frame.back", {})).handled === true;
97
+ }
98
+ dispose() {
99
+ window.removeEventListener("message", this.listener);
100
+ this.pending.forEach((p) => {
101
+ clearTimeout(p.timer);
102
+ p.reject(new Error("App frame disposed"));
103
+ });
104
+ this.pending.clear();
105
+ this.ready = false;
106
+ }
107
+ }
108
+ class AppFrameClient {
109
+ constructor(options) {
110
+ this.options = options;
111
+ this.listener = (event) => {
112
+ void this.receive(event);
113
+ };
114
+ this.origin = new URL(options.shellOrigin).origin;
115
+ this.nonce = new URLSearchParams(window.location.hash.slice(1)).get("bfp");
116
+ window.addEventListener("message", this.listener);
117
+ if (window.parent !== window && this.nonce) {
118
+ const hello = () => window.parent.postMessage(envelope("frame.hello", { nonce: this.nonce, appCapabilities: [
119
+ ...options.onOpen ? ["open"] : [],
120
+ ...options.onBeforeClose ? ["beforeClose"] : [],
121
+ ...options.onBack ? ["back"] : []
122
+ ] }), this.origin);
123
+ hello();
124
+ this.helloTimer = setInterval(hello, 250);
125
+ this.helloDeadline = setTimeout(() => clearInterval(this.helloTimer), 1500);
126
+ }
127
+ }
128
+ async receive(event) {
129
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n;
130
+ if (event.source !== window.parent || event.origin !== this.origin || !isMessage(event.data))
131
+ return;
132
+ const m = event.data;
133
+ const p = m.payload;
134
+ if (m.type === "frame.init") {
135
+ if (!this.nonce || p.nonce !== this.nonce || typeof p.locale !== "string" || !p.theme || typeof p.windowId !== "string" || p.launch && !isOpenRequest(p.launch))
136
+ return;
137
+ clearInterval(this.helloTimer);
138
+ clearTimeout(this.helloDeadline);
139
+ this.init = p;
140
+ (_b = (_a = this.options).onInit) == null ? void 0 : _b.call(_a, this.init);
141
+ return;
142
+ }
143
+ if (!this.init)
144
+ return;
145
+ const reply = (payload) => window.parent.postMessage(envelope(m.type + "Result", payload, { replyTo: m.id }), this.origin);
146
+ try {
147
+ if (m.type === "frame.open" && isOpenRequest(p.request))
148
+ reply(await ((_d = (_c = this.options).onOpen) == null ? void 0 : _d.call(_c, p.request)) ?? { accepted: false });
149
+ if (m.type === "frame.beforeClose")
150
+ reply({ decision: await ((_f = (_e = this.options).onBeforeClose) == null ? void 0 : _f.call(_e, p.reason)) ?? "allow" });
151
+ if (m.type === "frame.back")
152
+ reply({ handled: await ((_h = (_g = this.options).onBack) == null ? void 0 : _h.call(_g)) ?? false });
153
+ if (m.type === "frame.themeChanged" && (p.mode === "dark" || p.mode === "light"))
154
+ (_j = (_i = this.options).onTheme) == null ? void 0 : _j.call(_i, p);
155
+ if (m.type === "frame.localeChanged" && typeof p.locale === "string")
156
+ (_l = (_k = this.options).onLocale) == null ? void 0 : _l.call(_k, p.locale);
157
+ if (m.type === "frame.focusChanged")
158
+ (_n = (_m = this.options).onFocus) == null ? void 0 : _n.call(_m, p.focused === true);
159
+ } catch {
160
+ if (m.type === "frame.open")
161
+ reply({ accepted: false, fallback: "preview" });
162
+ if (m.type === "frame.beforeClose")
163
+ reply({ decision: "deny" });
164
+ }
165
+ }
166
+ send(type, payload) {
167
+ if (this.init)
168
+ window.parent.postMessage(envelope(type, payload), this.origin);
169
+ }
170
+ setTitle(title, dirty = false) {
171
+ this.send("window.setTitle", { title, dirty });
172
+ }
173
+ requestClose() {
174
+ this.send("window.requestClose", {});
175
+ }
176
+ openContent(request, target = "default", newWindow = false) {
177
+ this.send("content.open", { request, target, newWindow });
178
+ }
179
+ dispose() {
180
+ clearInterval(this.helloTimer);
181
+ clearTimeout(this.helloDeadline);
182
+ window.removeEventListener("message", this.listener);
183
+ }
184
+ }
185
+ export {
186
+ AppFrameClient,
187
+ AppFrameHost
188
+ };
189
+ //# sourceMappingURL=app-frame.mjs.map