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.
- package/cli/dist/cli.mjs +116 -31
- package/cli/dist/cli.mjs.map +1 -1
- package/cli/launcher.mjs +0 -0
- package/cli/modules/pikg.ts +5 -0
- package/cli/modules/pikg_protocol.ts +40 -0
- package/cli/runtime/host_deno.ts +16 -31
- package/cli/runtime/host_node.ts +15 -30
- package/cli/runtime/path_policy.ts +49 -0
- package/cli/version.ts +1 -1
- package/dist/app-frame.cjs +2 -0
- package/dist/app-frame.cjs.map +1 -0
- package/dist/app-frame.d.ts +82 -0
- package/dist/app-frame.d.ts.map +1 -0
- package/dist/app-frame.mjs +189 -0
- package/dist/app-frame.mjs.map +1 -0
- package/dist/browser.cjs +1 -1
- package/dist/browser.mjs +2 -2
- package/dist/content.cjs +2 -0
- package/dist/content.cjs.map +1 -0
- package/dist/content.d.ts +148 -0
- package/dist/content.d.ts.map +1 -0
- package/dist/content.mjs +361 -0
- package/dist/content.mjs.map +1 -0
- package/dist/content_mime.d.ts +4 -0
- package/dist/content_mime.d.ts.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.mjs +2 -2
- package/dist/{ndm_proxy-1b07ecf3.mjs → ndm_proxy-3caceb0a.mjs} +6 -2
- package/dist/{ndm_proxy-1b07ecf3.mjs.map → ndm_proxy-3caceb0a.mjs.map} +1 -1
- package/dist/{ndm_proxy-7b71deb4.js → ndm_proxy-d37a0204.js} +2 -2
- package/dist/{ndm_proxy-7b71deb4.js.map → ndm_proxy-d37a0204.js.map} +1 -1
- package/dist/nfsp.cjs +2 -0
- package/dist/nfsp.cjs.map +1 -0
- package/dist/nfsp.d.ts +484 -0
- package/dist/nfsp.d.ts.map +1 -0
- package/dist/nfsp.mjs +466 -0
- package/dist/nfsp.mjs.map +1 -0
- package/dist/node.cjs +1 -1
- package/dist/node.mjs +2 -2
- package/dist/runtime.d.ts.map +1 -1
- package/package.json +18 -2
package/cli/launcher.mjs
CHANGED
|
File without changes
|
package/cli/modules/pikg.ts
CHANGED
|
@@ -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
|
+
}
|
package/cli/runtime/host_deno.ts
CHANGED
|
@@ -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
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
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'
|
package/cli/runtime/host_node.ts
CHANGED
|
@@ -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
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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.
|
|
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
|