ugly-app 0.1.650 → 0.1.652
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/dist/cli/version.d.ts +1 -1
- package/dist/cli/version.js +1 -1
- package/dist/client/createSocket.d.ts.map +1 -1
- package/dist/client/createSocket.js +62 -0
- package/dist/client/createSocket.js.map +1 -1
- package/dist/native/server/binaries.d.ts +23 -0
- package/dist/native/server/binaries.d.ts.map +1 -0
- package/dist/native/server/binaries.js +125 -0
- package/dist/native/server/binaries.js.map +1 -0
- package/dist/native/server/index.d.ts +16 -0
- package/dist/native/server/index.d.ts.map +1 -0
- package/dist/native/server/index.js +16 -0
- package/dist/native/server/index.js.map +1 -0
- package/dist/native/server/permissions/audit.d.ts +29 -0
- package/dist/native/server/permissions/audit.d.ts.map +1 -0
- package/dist/native/server/permissions/audit.js +28 -0
- package/dist/native/server/permissions/audit.js.map +1 -0
- package/dist/native/server/permissions/gate.d.ts +63 -0
- package/dist/native/server/permissions/gate.d.ts.map +1 -0
- package/dist/native/server/permissions/gate.js +150 -0
- package/dist/native/server/permissions/gate.js.map +1 -0
- package/dist/native/server/permissions/grants.d.ts +36 -0
- package/dist/native/server/permissions/grants.d.ts.map +1 -0
- package/dist/native/server/permissions/grants.js +116 -0
- package/dist/native/server/permissions/grants.js.map +1 -0
- package/dist/native/server/permissions/scope.d.ts +32 -0
- package/dist/native/server/permissions/scope.d.ts.map +1 -0
- package/dist/native/server/permissions/scope.js +73 -0
- package/dist/native/server/permissions/scope.js.map +1 -0
- package/dist/native/server/permissions/types.d.ts +64 -0
- package/dist/native/server/permissions/types.d.ts.map +1 -0
- package/dist/native/server/permissions/types.js +25 -0
- package/dist/native/server/permissions/types.js.map +1 -0
- package/package.json +3 -2
- package/src/cli/version.ts +1 -1
- package/src/client/createSocket.ts +63 -0
- package/src/native/server/binaries.ts +136 -0
- package/src/native/server/index.ts +27 -0
- package/src/native/server/permissions/audit.ts +44 -0
- package/src/native/server/permissions/gate.test.ts +30 -0
- package/src/native/server/permissions/gate.ts +176 -0
- package/src/native/server/permissions/grants.test.ts +22 -0
- package/src/native/server/permissions/grants.ts +135 -0
- package/src/native/server/permissions/scope.ts +80 -0
- package/src/native/server/permissions/types.ts +75 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CapabilityGate — the single enforcement chokepoint the privileged
|
|
3
|
+
* daemon calls before performing any `window.ugly` operation. It
|
|
4
|
+
* composes the per-origin grants, the per-origin fs scope, the bundled-
|
|
5
|
+
* binary allowlist, and the audit log. Every call either returns the
|
|
6
|
+
* resolved/authorized value or throws `PermissionDenied` — and is
|
|
7
|
+
* recorded either way.
|
|
8
|
+
*
|
|
9
|
+
* Pure logic (no Electron / no fs): the Electron main wires the real
|
|
10
|
+
* `baseDir`, the live `GrantStore`, and an audit sink around it.
|
|
11
|
+
*/
|
|
12
|
+
import path from 'path';
|
|
13
|
+
import { AuditLog } from './audit.js';
|
|
14
|
+
import { originScopeDir, resolveScopedPath, ScopeViolation } from './scope.js';
|
|
15
|
+
export class PermissionDenied extends Error {
|
|
16
|
+
origin;
|
|
17
|
+
capability;
|
|
18
|
+
resource;
|
|
19
|
+
constructor(origin, capability, resource, reason) {
|
|
20
|
+
super(`Permission denied for ${origin} (${capability} ${resource}): ${reason}`);
|
|
21
|
+
this.origin = origin;
|
|
22
|
+
this.capability = capability;
|
|
23
|
+
this.resource = resource;
|
|
24
|
+
this.name = 'PermissionDenied';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export class CapabilityGate {
|
|
28
|
+
store;
|
|
29
|
+
baseDir;
|
|
30
|
+
bundled;
|
|
31
|
+
audit;
|
|
32
|
+
constructor(cfg) {
|
|
33
|
+
this.store = cfg.store;
|
|
34
|
+
this.baseDir = cfg.baseDir;
|
|
35
|
+
this.bundled = new Set(cfg.bundledBinaries);
|
|
36
|
+
this.audit = cfg.audit ?? new AuditLog();
|
|
37
|
+
}
|
|
38
|
+
/** The on-disk scope root for an origin (its sandboxed app folder). */
|
|
39
|
+
scopeRoot(origin) {
|
|
40
|
+
return originScopeDir(this.baseDir, origin);
|
|
41
|
+
}
|
|
42
|
+
deny(origin, capability, resource, reason) {
|
|
43
|
+
this.audit.record({ origin, capability, resource, allowed: false, reason });
|
|
44
|
+
throw new PermissionDenied(origin, capability, resource, reason);
|
|
45
|
+
}
|
|
46
|
+
allow(origin, capability, resource) {
|
|
47
|
+
this.audit.record({ origin, capability, resource, allowed: true });
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Authorize + resolve an `ugly.fs.*` path. 'scoped' confines to the
|
|
51
|
+
* origin's folder; 'full' (sudo) allows any absolute path; 'none'
|
|
52
|
+
* denies. Returns the absolute path to operate on.
|
|
53
|
+
*/
|
|
54
|
+
resolveFsPath(origin, requestedPath) {
|
|
55
|
+
const fs = this.store.query(origin).fs;
|
|
56
|
+
if (fs === 'none') {
|
|
57
|
+
this.deny(origin, 'fs', requestedPath, 'no filesystem permission');
|
|
58
|
+
}
|
|
59
|
+
if (fs === 'full') {
|
|
60
|
+
const abs = path.resolve(requestedPath);
|
|
61
|
+
this.allow(origin, 'fs:full', abs);
|
|
62
|
+
return abs;
|
|
63
|
+
}
|
|
64
|
+
// scoped
|
|
65
|
+
try {
|
|
66
|
+
const abs = resolveScopedPath(this.scopeRoot(origin), requestedPath);
|
|
67
|
+
this.allow(origin, 'fs:scoped', abs);
|
|
68
|
+
return abs;
|
|
69
|
+
}
|
|
70
|
+
catch (e) {
|
|
71
|
+
const reason = e instanceof ScopeViolation ? 'path escapes scoped folder' : 'invalid path';
|
|
72
|
+
this.deny(origin, 'fs:scoped', requestedPath, reason);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Resolve + confine a process's working directory. A `process` grant
|
|
77
|
+
* alone is enough to run inside the origin's scope folder — this does
|
|
78
|
+
* NOT require a separate `fs` grant (that gates `ugly.fs.*`). A 'full'
|
|
79
|
+
* (sudo) origin may cwd anywhere; everyone else is confined to scope.
|
|
80
|
+
*/
|
|
81
|
+
resolveProcessCwd(origin, cwd) {
|
|
82
|
+
const root = this.scopeRoot(origin);
|
|
83
|
+
if (!cwd)
|
|
84
|
+
return root;
|
|
85
|
+
if (this.store.query(origin).fs === 'full') {
|
|
86
|
+
const abs = path.resolve(cwd);
|
|
87
|
+
this.allow(origin, 'process:cwd', abs);
|
|
88
|
+
return abs;
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
const abs = resolveScopedPath(root, cwd);
|
|
92
|
+
this.allow(origin, 'process:cwd', abs);
|
|
93
|
+
return abs;
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
this.deny(origin, 'process:cwd', cwd, 'cwd escapes scoped folder');
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/** Does this origin hold the github capability (authorizes gh + git)? */
|
|
100
|
+
holdsGithub(origin) {
|
|
101
|
+
return this.store.query(origin).github;
|
|
102
|
+
}
|
|
103
|
+
/** Authorize an `ugly.process` spawn of `binary`. Throws if not allowed. */
|
|
104
|
+
checkProcess(origin, binary) {
|
|
105
|
+
if (!this.bundled.has(binary)) {
|
|
106
|
+
this.deny(origin, 'process', binary, 'not a bundled tool');
|
|
107
|
+
}
|
|
108
|
+
const g = this.store.query(origin);
|
|
109
|
+
// The github capability authorizes exactly the GitHub CLIs.
|
|
110
|
+
const githubTool = g.github && (binary === 'gh' || binary === 'git');
|
|
111
|
+
if (!githubTool && !g.process.includes(binary)) {
|
|
112
|
+
this.deny(origin, 'process', binary, 'tool not granted');
|
|
113
|
+
}
|
|
114
|
+
this.allow(origin, 'process', binary);
|
|
115
|
+
}
|
|
116
|
+
checkServe(origin, port = 0) {
|
|
117
|
+
if (!this.store.query(origin).serve) {
|
|
118
|
+
this.deny(origin, 'serve', String(port), 'serve not granted');
|
|
119
|
+
}
|
|
120
|
+
this.allow(origin, 'serve', String(port));
|
|
121
|
+
}
|
|
122
|
+
checkNet(origin, target = '') {
|
|
123
|
+
if (!this.store.query(origin).net) {
|
|
124
|
+
this.deny(origin, 'net', target, 'net not granted');
|
|
125
|
+
}
|
|
126
|
+
this.allow(origin, 'net', target);
|
|
127
|
+
}
|
|
128
|
+
checkWorkers(origin, opts = {}) {
|
|
129
|
+
const g = this.store.query(origin);
|
|
130
|
+
if (!g.workers)
|
|
131
|
+
this.deny(origin, 'workers', '', 'workers not granted');
|
|
132
|
+
if (opts.boot && !g.workersBoot) {
|
|
133
|
+
this.deny(origin, 'workers:boot', '', 'boot-start not granted');
|
|
134
|
+
}
|
|
135
|
+
this.allow(origin, opts.boot ? 'workers:boot' : 'workers', '');
|
|
136
|
+
}
|
|
137
|
+
checkSecrets(origin, key = '') {
|
|
138
|
+
if (!this.store.query(origin).secrets) {
|
|
139
|
+
this.deny(origin, 'secrets', key, 'secrets not granted');
|
|
140
|
+
}
|
|
141
|
+
this.allow(origin, 'secrets', key);
|
|
142
|
+
}
|
|
143
|
+
checkBrowse(origin, target = '') {
|
|
144
|
+
if (!this.store.query(origin).browse) {
|
|
145
|
+
this.deny(origin, 'browse', target, 'browse not granted');
|
|
146
|
+
}
|
|
147
|
+
this.allow(origin, 'browse', target);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
//# sourceMappingURL=gate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gate.js","sourceRoot":"","sources":["../../../../src/native/server/permissions/gate.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-origin grant store + the incremental request decision logic.
|
|
3
|
+
*
|
|
4
|
+
* `evaluate()` is pure: it diffs a request against what the origin holds
|
|
5
|
+
* and reports the delta + whether a prompt / elevated confirm is needed,
|
|
6
|
+
* WITHOUT mutating. The Electron layer shows the prompt for the delta,
|
|
7
|
+
* then calls `grant()` to commit (or auto-commits for trusted origins).
|
|
8
|
+
* This keeps "decide" separate from "persist" and makes both testable.
|
|
9
|
+
*/
|
|
10
|
+
import { type CapabilityRequest, type OriginGrant, type PermissionDecision } from './types.js';
|
|
11
|
+
export declare class GrantStore {
|
|
12
|
+
private readonly grants;
|
|
13
|
+
private readonly trusted;
|
|
14
|
+
constructor(trustedOrigins?: readonly string[]);
|
|
15
|
+
isTrusted(origin: string): boolean;
|
|
16
|
+
/** Capabilities the origin currently holds (EMPTY_GRANT if none). */
|
|
17
|
+
query(origin: string): OriginGrant;
|
|
18
|
+
/**
|
|
19
|
+
* Decide what a request needs — the delta beyond what's held, and
|
|
20
|
+
* whether it must prompt / elevate. Pure: does not mutate.
|
|
21
|
+
*/
|
|
22
|
+
evaluate(origin: string, req: CapabilityRequest): PermissionDecision;
|
|
23
|
+
/**
|
|
24
|
+
* Commit a (delta) grant onto the origin's held set. Idempotent and
|
|
25
|
+
* monotonic — merges, never downgrades. Returns the new held grant.
|
|
26
|
+
*/
|
|
27
|
+
grant(origin: string, req: CapabilityRequest): OriginGrant;
|
|
28
|
+
/** Revoke everything for an origin (Subsystem B exposes a settings UI). */
|
|
29
|
+
revoke(origin: string): void;
|
|
30
|
+
/** Serializable copy of all held grants (for persistence). */
|
|
31
|
+
snapshot(): Record<string, OriginGrant>;
|
|
32
|
+
/** Replace all held grants from a snapshot (load on startup). */
|
|
33
|
+
restore(snapshot: Record<string, OriginGrant>): void;
|
|
34
|
+
}
|
|
35
|
+
export type GrantSnapshot = Record<string, OriginGrant>;
|
|
36
|
+
//# sourceMappingURL=grants.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"grants.d.ts","sourceRoot":"","sources":["../../../../src/native/server/permissions/grants.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EACL,KAAK,iBAAiB,EAEtB,KAAK,WAAW,EAChB,KAAK,kBAAkB,EACxB,MAAM,YAAY,CAAC;AAapB,qBAAa,UAAU;IACrB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAkC;IACzD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAsB;gBAElC,cAAc,GAAE,SAAS,MAAM,EAAO;IAIlD,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO;IAIlC,qEAAqE;IACrE,KAAK,CAAC,MAAM,EAAE,MAAM,GAAG,WAAW;IAIlC;;;OAGG;IACH,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,iBAAiB,GAAG,kBAAkB;IAyCpE;;;OAGG;IACH,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,iBAAiB,GAAG,WAAW;IAmB1D,2EAA2E;IAC3E,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAI5B,8DAA8D;IAC9D,QAAQ,IAAI,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC;IAMvC,iEAAiE;IACjE,OAAO,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,GAAG,IAAI;CAMrD;AAED,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC"}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-origin grant store + the incremental request decision logic.
|
|
3
|
+
*
|
|
4
|
+
* `evaluate()` is pure: it diffs a request against what the origin holds
|
|
5
|
+
* and reports the delta + whether a prompt / elevated confirm is needed,
|
|
6
|
+
* WITHOUT mutating. The Electron layer shows the prompt for the delta,
|
|
7
|
+
* then calls `grant()` to commit (or auto-commits for trusted origins).
|
|
8
|
+
* This keeps "decide" separate from "persist" and makes both testable.
|
|
9
|
+
*/
|
|
10
|
+
import { EMPTY_GRANT, } from './types.js';
|
|
11
|
+
function normalizeWorkers(w) {
|
|
12
|
+
if (w === undefined)
|
|
13
|
+
return { workers: false, boot: false };
|
|
14
|
+
if (typeof w === 'boolean')
|
|
15
|
+
return { workers: w, boot: false };
|
|
16
|
+
return { workers: true, boot: w.boot === true };
|
|
17
|
+
}
|
|
18
|
+
/** fs rank so 'full' supersedes 'scoped' supersedes 'none'. */
|
|
19
|
+
const FS_RANK = { none: 0, scoped: 1, full: 2 };
|
|
20
|
+
export class GrantStore {
|
|
21
|
+
grants = new Map();
|
|
22
|
+
trusted;
|
|
23
|
+
constructor(trustedOrigins = []) {
|
|
24
|
+
this.trusted = new Set(trustedOrigins);
|
|
25
|
+
}
|
|
26
|
+
isTrusted(origin) {
|
|
27
|
+
return this.trusted.has(origin);
|
|
28
|
+
}
|
|
29
|
+
/** Capabilities the origin currently holds (EMPTY_GRANT if none). */
|
|
30
|
+
query(origin) {
|
|
31
|
+
return this.grants.get(origin) ?? EMPTY_GRANT;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Decide what a request needs — the delta beyond what's held, and
|
|
35
|
+
* whether it must prompt / elevate. Pure: does not mutate.
|
|
36
|
+
*/
|
|
37
|
+
evaluate(origin, req) {
|
|
38
|
+
const held = this.query(origin);
|
|
39
|
+
const delta = {};
|
|
40
|
+
// fs: only a STRICT increase in level is new (scoped→full is new;
|
|
41
|
+
// full→scoped is already covered).
|
|
42
|
+
if (req.fs && FS_RANK[req.fs] > FS_RANK[held.fs]) {
|
|
43
|
+
delta.fs = req.fs;
|
|
44
|
+
}
|
|
45
|
+
// process: binaries not already granted.
|
|
46
|
+
if (req.process && req.process.length > 0) {
|
|
47
|
+
const have = new Set(held.process);
|
|
48
|
+
const missing = [...new Set(req.process)].filter((p) => !have.has(p));
|
|
49
|
+
if (missing.length > 0)
|
|
50
|
+
delta.process = missing.sort();
|
|
51
|
+
}
|
|
52
|
+
if (req.serve && !held.serve)
|
|
53
|
+
delta.serve = true;
|
|
54
|
+
if (req.net && !held.net)
|
|
55
|
+
delta.net = true;
|
|
56
|
+
if (req.secrets && !held.secrets)
|
|
57
|
+
delta.secrets = true;
|
|
58
|
+
if (req.browse && !held.browse)
|
|
59
|
+
delta.browse = true;
|
|
60
|
+
if (req.github && !held.github)
|
|
61
|
+
delta.github = true;
|
|
62
|
+
const wantW = normalizeWorkers(req.workers);
|
|
63
|
+
if ((wantW.workers && !held.workers) || (wantW.boot && !held.workersBoot)) {
|
|
64
|
+
delta.workers = wantW.boot ? { boot: true } : true;
|
|
65
|
+
}
|
|
66
|
+
const isEmpty = Object.keys(delta).length === 0;
|
|
67
|
+
const elevated = delta.fs === 'full' ||
|
|
68
|
+
(typeof delta.workers === 'object' && delta.workers.boot === true);
|
|
69
|
+
return {
|
|
70
|
+
alreadyHeld: isEmpty,
|
|
71
|
+
delta,
|
|
72
|
+
requiresPrompt: !isEmpty && !this.isTrusted(origin),
|
|
73
|
+
requiresElevatedConfirm: !isEmpty && elevated,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Commit a (delta) grant onto the origin's held set. Idempotent and
|
|
78
|
+
* monotonic — merges, never downgrades. Returns the new held grant.
|
|
79
|
+
*/
|
|
80
|
+
grant(origin, req) {
|
|
81
|
+
const held = this.query(origin);
|
|
82
|
+
const wantW = normalizeWorkers(req.workers);
|
|
83
|
+
const next = {
|
|
84
|
+
fs: req.fs && FS_RANK[req.fs] > FS_RANK[held.fs] ? req.fs : held.fs,
|
|
85
|
+
process: [...new Set([...held.process, ...(req.process ?? [])])].sort(),
|
|
86
|
+
serve: held.serve || req.serve === true,
|
|
87
|
+
workers: held.workers || wantW.workers,
|
|
88
|
+
workersBoot: held.workersBoot || wantW.boot,
|
|
89
|
+
net: held.net || req.net === true,
|
|
90
|
+
secrets: held.secrets || req.secrets === true,
|
|
91
|
+
browse: held.browse || req.browse === true,
|
|
92
|
+
github: held.github || req.github === true,
|
|
93
|
+
};
|
|
94
|
+
this.grants.set(origin, next);
|
|
95
|
+
return next;
|
|
96
|
+
}
|
|
97
|
+
/** Revoke everything for an origin (Subsystem B exposes a settings UI). */
|
|
98
|
+
revoke(origin) {
|
|
99
|
+
this.grants.delete(origin);
|
|
100
|
+
}
|
|
101
|
+
/** Serializable copy of all held grants (for persistence). */
|
|
102
|
+
snapshot() {
|
|
103
|
+
const out = {};
|
|
104
|
+
for (const [origin, g] of this.grants)
|
|
105
|
+
out[origin] = { ...g, process: [...g.process] };
|
|
106
|
+
return out;
|
|
107
|
+
}
|
|
108
|
+
/** Replace all held grants from a snapshot (load on startup). */
|
|
109
|
+
restore(snapshot) {
|
|
110
|
+
this.grants.clear();
|
|
111
|
+
for (const [origin, g] of Object.entries(snapshot)) {
|
|
112
|
+
this.grants.set(origin, { ...g, process: [...g.process].sort() });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=grants.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"grants.js","sourceRoot":"","sources":["../../../../src/native/server/permissions/grants.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filesystem scope confinement for `ugly.fs.*`.
|
|
3
|
+
*
|
|
4
|
+
* A scoped origin may only touch paths inside its per-origin folder
|
|
5
|
+
* (`<base>/apps/<hash>/`). `resolveScopedPath` is the single chokepoint
|
|
6
|
+
* every scoped fs call passes through: it resolves the requested path
|
|
7
|
+
* (relative → against the scope root; absolute → as-is) and rejects
|
|
8
|
+
* anything that lands outside the root — `..` traversal, an absolute
|
|
9
|
+
* path elsewhere, or a NUL byte. This is pure path math; symlink
|
|
10
|
+
* escapes are caught by a second `realpath` check at the daemon (it
|
|
11
|
+
* needs fs), layered on top of this.
|
|
12
|
+
*/
|
|
13
|
+
export declare class ScopeViolation extends Error {
|
|
14
|
+
readonly requested: string;
|
|
15
|
+
readonly scopeRoot: string;
|
|
16
|
+
constructor(requested: string, scopeRoot: string);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Deterministic per-origin folder name. A normal origin
|
|
20
|
+
* (`https://app.example.com`) maps to a stable, filesystem-safe slug +
|
|
21
|
+
* short hash so two origins can't collide and the folder is recognizable.
|
|
22
|
+
*/
|
|
23
|
+
export declare function originScopeDir(baseDir: string, origin: string): string;
|
|
24
|
+
/**
|
|
25
|
+
* Resolve `requested` against `scopeRoot` and confirm it stays inside.
|
|
26
|
+
* Throws `ScopeViolation` on any escape. Returns the absolute,
|
|
27
|
+
* normalized path on success.
|
|
28
|
+
*/
|
|
29
|
+
export declare function resolveScopedPath(scopeRoot: string, requested: string): string;
|
|
30
|
+
/** True when the resolved path is inside the scope root (no throw). */
|
|
31
|
+
export declare function isWithinScope(scopeRoot: string, requested: string): boolean;
|
|
32
|
+
//# sourceMappingURL=scope.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scope.d.ts","sourceRoot":"","sources":["../../../../src/native/server/permissions/scope.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH,qBAAa,cAAe,SAAQ,KAAK;IAErC,QAAQ,CAAC,SAAS,EAAE,MAAM;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM;gBADjB,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM;CAS7B;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAQtE;AAaD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAS9E;AAED,uEAAuE;AACvE,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAO3E"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filesystem scope confinement for `ugly.fs.*`.
|
|
3
|
+
*
|
|
4
|
+
* A scoped origin may only touch paths inside its per-origin folder
|
|
5
|
+
* (`<base>/apps/<hash>/`). `resolveScopedPath` is the single chokepoint
|
|
6
|
+
* every scoped fs call passes through: it resolves the requested path
|
|
7
|
+
* (relative → against the scope root; absolute → as-is) and rejects
|
|
8
|
+
* anything that lands outside the root — `..` traversal, an absolute
|
|
9
|
+
* path elsewhere, or a NUL byte. This is pure path math; symlink
|
|
10
|
+
* escapes are caught by a second `realpath` check at the daemon (it
|
|
11
|
+
* needs fs), layered on top of this.
|
|
12
|
+
*/
|
|
13
|
+
import path from 'path';
|
|
14
|
+
export class ScopeViolation extends Error {
|
|
15
|
+
requested;
|
|
16
|
+
scopeRoot;
|
|
17
|
+
constructor(requested, scopeRoot) {
|
|
18
|
+
super(`Path escapes the app's scoped folder: ${JSON.stringify(requested)} is outside ${scopeRoot}`);
|
|
19
|
+
this.requested = requested;
|
|
20
|
+
this.scopeRoot = scopeRoot;
|
|
21
|
+
this.name = 'ScopeViolation';
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Deterministic per-origin folder name. A normal origin
|
|
26
|
+
* (`https://app.example.com`) maps to a stable, filesystem-safe slug +
|
|
27
|
+
* short hash so two origins can't collide and the folder is recognizable.
|
|
28
|
+
*/
|
|
29
|
+
export function originScopeDir(baseDir, origin) {
|
|
30
|
+
const slug = origin
|
|
31
|
+
.replace(/^[a-z]+:\/\//i, '')
|
|
32
|
+
.replace(/[^a-zA-Z0-9.-]+/g, '_')
|
|
33
|
+
.replace(/^_+|_+$/g, '')
|
|
34
|
+
.slice(0, 40) || 'origin';
|
|
35
|
+
return path.join(baseDir, 'apps', `${slug}-${shortHash(origin)}`);
|
|
36
|
+
}
|
|
37
|
+
/** Small, dependency-free, stable string hash → 8 hex chars. */
|
|
38
|
+
function shortHash(s) {
|
|
39
|
+
// FNV-1a 32-bit.
|
|
40
|
+
let h = 0x811c9dc5;
|
|
41
|
+
for (let i = 0; i < s.length; i++) {
|
|
42
|
+
h ^= s.charCodeAt(i);
|
|
43
|
+
h = Math.imul(h, 0x01000193);
|
|
44
|
+
}
|
|
45
|
+
return (h >>> 0).toString(16).padStart(8, '0');
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Resolve `requested` against `scopeRoot` and confirm it stays inside.
|
|
49
|
+
* Throws `ScopeViolation` on any escape. Returns the absolute,
|
|
50
|
+
* normalized path on success.
|
|
51
|
+
*/
|
|
52
|
+
export function resolveScopedPath(scopeRoot, requested) {
|
|
53
|
+
if (requested.includes('\0'))
|
|
54
|
+
throw new ScopeViolation(requested, scopeRoot);
|
|
55
|
+
const root = path.resolve(scopeRoot);
|
|
56
|
+
// path.resolve collapses `..`; an absolute `requested` replaces root.
|
|
57
|
+
const resolved = path.resolve(root, requested);
|
|
58
|
+
if (resolved !== root && !resolved.startsWith(root + path.sep)) {
|
|
59
|
+
throw new ScopeViolation(requested, scopeRoot);
|
|
60
|
+
}
|
|
61
|
+
return resolved;
|
|
62
|
+
}
|
|
63
|
+
/** True when the resolved path is inside the scope root (no throw). */
|
|
64
|
+
export function isWithinScope(scopeRoot, requested) {
|
|
65
|
+
try {
|
|
66
|
+
resolveScopedPath(scopeRoot, requested);
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=scope.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scope.js","sourceRoot":"","sources":["../../../../src/native/server/permissions/scope.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,IAAI,MAAM,MAAM,CAAC;AAExB,MAAM,OAAO,cAAe,SAAQ,KAAK;IAE5B;IACA;IAFX,YACW,SAAiB,EACjB,SAAiB;QAE1B,KAAK,CACH,yCAAyC,IAAI,CAAC,SAAS,CACrD,SAAS,CACV,eAAe,SAAS,EAAE,CAC5B,CAAC;QAPO,cAAS,GAAT,SAAS,CAAQ;QACjB,cAAS,GAAT,SAAS,CAAQ;QAO1B,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAAC;IAC/B,CAAC;CACF;AAED;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,OAAe,EAAE,MAAc;IAC5D,MAAM,IAAI,GACR,MAAM;SACH,OAAO,CAAC,eAAe,EAAE,EAAE,CAAC;SAC5B,OAAO,CAAC,kBAAkB,EAAE,GAAG,CAAC;SAChC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC;SACvB,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,QAAQ,CAAC;IAC9B,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,IAAI,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;AACpE,CAAC;AAED,gEAAgE;AAChE,SAAS,SAAS,CAAC,CAAS;IAC1B,iBAAiB;IACjB,IAAI,CAAC,GAAG,UAAU,CAAC;IACnB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAClC,CAAC,IAAI,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;QACrB,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC;IAC/B,CAAC;IACD,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;AACjD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,SAAiB,EAAE,SAAiB;IACpE,IAAI,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC;QAAE,MAAM,IAAI,cAAc,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;IAC7E,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IACrC,sEAAsE;IACtE,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;IAC/C,IAAI,QAAQ,KAAK,IAAI,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;QAC/D,MAAM,IAAI,cAAc,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;IACjD,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,uEAAuE;AACvE,MAAM,UAAU,aAAa,CAAC,SAAiB,EAAE,SAAiB;IAChE,IAAI,CAAC;QACH,iBAAiB,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;QACxC,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Permission model for the Ugly Browser's privileged capabilities.
|
|
3
|
+
*
|
|
4
|
+
* Every `window.ugly` call is gated against a per-origin grant. This
|
|
5
|
+
* module is pure decision logic — no Electron, no IO — so the security
|
|
6
|
+
* crux is unit-testable in isolation. The Electron main process wires
|
|
7
|
+
* the prompt UI + on-disk persistence around it (Subsystem B).
|
|
8
|
+
*
|
|
9
|
+
* Tiers (see the plan): a trusted-origin allowlist auto-grants; any
|
|
10
|
+
* other origin must be prompted and is confined to a per-origin scoped
|
|
11
|
+
* folder; `fs: 'full'` (sudo) and `workers.boot` (start-on-OS-boot) are
|
|
12
|
+
* separate, scarier confirmations layered on top.
|
|
13
|
+
*/
|
|
14
|
+
/** What a site asks for via `ugly.permissions.request(...)`. */
|
|
15
|
+
export interface CapabilityRequest {
|
|
16
|
+
/** 'scoped' = per-origin folder only; 'full' = entire filesystem (sudo). */
|
|
17
|
+
fs?: 'scoped' | 'full';
|
|
18
|
+
/** Bundled binaries the site wants to run (e.g. ['python', 'ffmpeg']). */
|
|
19
|
+
process?: readonly string[];
|
|
20
|
+
/** Bind a localhost server. */
|
|
21
|
+
serve?: boolean;
|
|
22
|
+
/** Register a background worker, optionally start-on-boot. */
|
|
23
|
+
workers?: boolean | {
|
|
24
|
+
boot?: boolean;
|
|
25
|
+
};
|
|
26
|
+
/** Raw TCP/UDP sockets (outbound connect, and later listen). */
|
|
27
|
+
net?: boolean;
|
|
28
|
+
/** OS keychain access. */
|
|
29
|
+
secrets?: boolean;
|
|
30
|
+
/** Drive headless web pages: load arbitrary URLs, extract, navigate, automate. */
|
|
31
|
+
browse?: boolean;
|
|
32
|
+
/** Use Ugly Studio's single GitHub account via gh/git (Device-Flow auth). */
|
|
33
|
+
github?: boolean;
|
|
34
|
+
}
|
|
35
|
+
/** The resolved set of capabilities an origin currently holds. */
|
|
36
|
+
export interface OriginGrant {
|
|
37
|
+
fs: 'none' | 'scoped' | 'full';
|
|
38
|
+
/** Granted binaries, sorted + de-duped. */
|
|
39
|
+
process: readonly string[];
|
|
40
|
+
serve: boolean;
|
|
41
|
+
workers: boolean;
|
|
42
|
+
workersBoot: boolean;
|
|
43
|
+
net: boolean;
|
|
44
|
+
secrets: boolean;
|
|
45
|
+
browse: boolean;
|
|
46
|
+
github: boolean;
|
|
47
|
+
}
|
|
48
|
+
export declare const EMPTY_GRANT: OriginGrant;
|
|
49
|
+
/**
|
|
50
|
+
* The decision returned by evaluating a request against held grants.
|
|
51
|
+
* `delta` is what's NEW (the part to prompt for); when it's empty the
|
|
52
|
+
* request is satisfied silently (idempotent / just-in-time semantics).
|
|
53
|
+
*/
|
|
54
|
+
export interface PermissionDecision {
|
|
55
|
+
/** True when the held grant already covers the whole request. */
|
|
56
|
+
alreadyHeld: boolean;
|
|
57
|
+
/** Only the capabilities not already held — what a prompt would ask for. */
|
|
58
|
+
delta: CapabilityRequest;
|
|
59
|
+
/** A prompt is needed (non-empty delta AND origin not trusted). */
|
|
60
|
+
requiresPrompt: boolean;
|
|
61
|
+
/** The delta includes a sudo-class capability (full FS or boot worker). */
|
|
62
|
+
requiresElevatedConfirm: boolean;
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/native/server/permissions/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,gEAAgE;AAChE,MAAM,WAAW,iBAAiB;IAChC,4EAA4E;IAC5E,EAAE,CAAC,EAAE,QAAQ,GAAG,MAAM,CAAC;IACvB,0EAA0E;IAC1E,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B,+BAA+B;IAC/B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,8DAA8D;IAC9D,OAAO,CAAC,EAAE,OAAO,GAAG;QAAE,IAAI,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;IACvC,gEAAgE;IAChE,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,0BAA0B;IAC1B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,kFAAkF;IAClF,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,6EAA6E;IAC7E,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,kEAAkE;AAClE,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,GAAG,QAAQ,GAAG,MAAM,CAAC;IAC/B,2CAA2C;IAC3C,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3B,KAAK,EAAE,OAAO,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;IACjB,WAAW,EAAE,OAAO,CAAC;IACrB,GAAG,EAAE,OAAO,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,OAAO,CAAC;IAChB,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,eAAO,MAAM,WAAW,EAAE,WAUzB,CAAC;AAEF;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IACjC,iEAAiE;IACjE,WAAW,EAAE,OAAO,CAAC;IACrB,4EAA4E;IAC5E,KAAK,EAAE,iBAAiB,CAAC;IACzB,mEAAmE;IACnE,cAAc,EAAE,OAAO,CAAC;IACxB,2EAA2E;IAC3E,uBAAuB,EAAE,OAAO,CAAC;CAClC"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Permission model for the Ugly Browser's privileged capabilities.
|
|
3
|
+
*
|
|
4
|
+
* Every `window.ugly` call is gated against a per-origin grant. This
|
|
5
|
+
* module is pure decision logic — no Electron, no IO — so the security
|
|
6
|
+
* crux is unit-testable in isolation. The Electron main process wires
|
|
7
|
+
* the prompt UI + on-disk persistence around it (Subsystem B).
|
|
8
|
+
*
|
|
9
|
+
* Tiers (see the plan): a trusted-origin allowlist auto-grants; any
|
|
10
|
+
* other origin must be prompted and is confined to a per-origin scoped
|
|
11
|
+
* folder; `fs: 'full'` (sudo) and `workers.boot` (start-on-OS-boot) are
|
|
12
|
+
* separate, scarier confirmations layered on top.
|
|
13
|
+
*/
|
|
14
|
+
export const EMPTY_GRANT = {
|
|
15
|
+
fs: 'none',
|
|
16
|
+
process: [],
|
|
17
|
+
serve: false,
|
|
18
|
+
workers: false,
|
|
19
|
+
workersBoot: false,
|
|
20
|
+
net: false,
|
|
21
|
+
secrets: false,
|
|
22
|
+
browse: false,
|
|
23
|
+
github: false,
|
|
24
|
+
};
|
|
25
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../../src/native/server/permissions/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAoCH,MAAM,CAAC,MAAM,WAAW,GAAgB;IACtC,EAAE,EAAE,MAAM;IACV,OAAO,EAAE,EAAE;IACX,KAAK,EAAE,KAAK;IACZ,OAAO,EAAE,KAAK;IACd,WAAW,EAAE,KAAK;IAClB,GAAG,EAAE,KAAK;IACV,OAAO,EAAE,KAAK;IACd,MAAM,EAAE,KAAK;IACb,MAAM,EAAE,KAAK;CACd,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ugly-app",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.652",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"comment:files": "Allowlist what ships to npm. dist = runtime; src = sourcemap targets (dist/*.js.map reference ../../src/); templates = CLI scaffold. Everything else at repo root (.pgdata local Postgres, coverage, assets/icons sources, test/, test-results/) is excluded by omission. The !negations strip the scaffold's installed deps + cruft (templates/node_modules is 200MB+ and must never ship). package.json/README/LICENSE ship automatically.",
|
|
6
6
|
"files": [
|
|
@@ -48,7 +48,8 @@
|
|
|
48
48
|
"./vite": "./dist/vite/index.js",
|
|
49
49
|
"./eslint": "./dist/eslint/index.js",
|
|
50
50
|
"./native": "./dist/native/index.js",
|
|
51
|
-
"./native/conformance": "./dist/native/conformance/harness.js"
|
|
51
|
+
"./native/conformance": "./dist/native/conformance/harness.js",
|
|
52
|
+
"./native/server": "./dist/native/server/index.js"
|
|
52
53
|
},
|
|
53
54
|
"bin": {
|
|
54
55
|
"ugly-app": "dist/cli/index.js"
|
package/src/cli/version.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Auto-generated by prebuild — do not edit manually
|
|
2
|
-
export const CLI_VERSION = "0.1.
|
|
2
|
+
export const CLI_VERSION = "0.1.652";
|
|
@@ -590,6 +590,69 @@ export function createSocket<R extends AppRegistryBase>(
|
|
|
590
590
|
>;
|
|
591
591
|
},
|
|
592
592
|
|
|
593
|
+
/**
|
|
594
|
+
* A STREAMED request over HTTP SSE (not the WS): used by `runAgent` so agent
|
|
595
|
+
* turns deliver `msg:delta` frames live without needing a `trackDocs` hub
|
|
596
|
+
* channel (apps don't register `agentStream`). POSTs to `/api/<name>` with
|
|
597
|
+
* `Accept: text/event-stream` (the framework's SSE-response mode), forwards
|
|
598
|
+
* each agent frame to `onFrame`, and resolves with the terminal `__result__`.
|
|
599
|
+
* Auth rides the same-origin `auth_token` cookie (`credentials: 'include'`).
|
|
600
|
+
*/
|
|
601
|
+
async requestStream(
|
|
602
|
+
name: string,
|
|
603
|
+
input: unknown,
|
|
604
|
+
onFrame: (frame: unknown) => void,
|
|
605
|
+
opts?: { signal?: AbortSignal },
|
|
606
|
+
): Promise<unknown> {
|
|
607
|
+
const res = await fetch(`/api/${name}`, {
|
|
608
|
+
method: 'POST',
|
|
609
|
+
headers: { 'Content-Type': 'application/json', Accept: 'text/event-stream' },
|
|
610
|
+
credentials: 'include',
|
|
611
|
+
body: JSON.stringify({ input }),
|
|
612
|
+
...(opts?.signal ? { signal: opts.signal } : {}),
|
|
613
|
+
});
|
|
614
|
+
if (!res.ok || !res.body) {
|
|
615
|
+
// No stream available — fall back to the buffered JSON result.
|
|
616
|
+
const json = (await res.json().catch(() => ({}))) as { result?: unknown; error?: string };
|
|
617
|
+
if (json.error) throw new Error(json.error);
|
|
618
|
+
return json.result;
|
|
619
|
+
}
|
|
620
|
+
const reader = res.body.getReader();
|
|
621
|
+
const decoder = new TextDecoder('utf-8');
|
|
622
|
+
let buffer = '';
|
|
623
|
+
let result: unknown;
|
|
624
|
+
let done = false;
|
|
625
|
+
while (!done) {
|
|
626
|
+
const { value, done: rdone } = await reader.read();
|
|
627
|
+
if (rdone) break;
|
|
628
|
+
buffer += decoder.decode(value, { stream: true });
|
|
629
|
+
let nl: number;
|
|
630
|
+
while ((nl = buffer.indexOf('\n')) !== -1) {
|
|
631
|
+
const line = buffer.slice(0, nl).replace(/\r$/, '');
|
|
632
|
+
buffer = buffer.slice(nl + 1);
|
|
633
|
+
if (!line.startsWith('data:')) continue;
|
|
634
|
+
const data = line.slice(5).trimStart();
|
|
635
|
+
if (!data) continue;
|
|
636
|
+
let frame: { type?: string; result?: unknown; error?: string };
|
|
637
|
+
try {
|
|
638
|
+
frame = JSON.parse(data) as typeof frame;
|
|
639
|
+
} catch {
|
|
640
|
+
continue;
|
|
641
|
+
}
|
|
642
|
+
if (frame.type === '__result__') {
|
|
643
|
+
result = frame.result;
|
|
644
|
+
done = true;
|
|
645
|
+
break;
|
|
646
|
+
}
|
|
647
|
+
if (frame.type === '__error__') {
|
|
648
|
+
throw new Error(frame.error ?? `request ${name} failed`);
|
|
649
|
+
}
|
|
650
|
+
onFrame(frame);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
return result;
|
|
654
|
+
},
|
|
655
|
+
|
|
593
656
|
getDoc<T extends DBObject>(collection: string, id: string): Promise<T | null> {
|
|
594
657
|
return rpc('request', { name: 'storeGetDoc', input: { collection, id } }).then(
|
|
595
658
|
(r) => (r as { item: T | null }).item,
|