@agentshouse/app-bridge 0.1.0-cac36ee240fcd → 0.1.0-cbcf384385682
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/README.md +17 -12
- package/dist/calls.d.ts +117 -0
- package/dist/calls.js +1 -0
- package/dist/index.d.ts +137 -0
- package/dist/index.js +1 -0
- package/dist/{client/src/test-host.d.ts → test-host.d.ts} +3 -7
- package/dist/test-host.js +173 -0
- package/docs/families.md +39 -34
- package/package.json +7 -7
- package/dist/client/src/bridge.d.ts +0 -38
- package/dist/client/src/bridge.js +0 -320
- package/dist/client/src/index.d.ts +0 -3
- package/dist/client/src/index.js +0 -2
- package/dist/client/src/test-host.js +0 -185
- package/dist/client/src/wire.d.ts +0 -211
- package/dist/client/src/wire.js +0 -1
- package/dist/src/apps/remainder.d.ts +0 -2
- package/dist/src/apps/remainder.js +0 -16
- package/dist/src/apps/tab.d.ts +0 -6
- package/dist/src/apps/tab.js +0 -27
package/README.md
CHANGED
|
@@ -1,22 +1,25 @@
|
|
|
1
1
|
# @agentshouse/app-bridge
|
|
2
2
|
|
|
3
|
-
The
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
The types of the agents.house App bridge, an accessor and an in-page test host.
|
|
4
|
+
House builds the bridge object in every App it serves and hands it to the page
|
|
5
|
+
before the App's own scripts run; `houseBridge()` returns that object, so an App
|
|
6
|
+
carries no bridge code of its own. It carries eleven families: navigation,
|
|
7
|
+
ceremonies, installation, notifications, status, Site image reads, App release
|
|
8
|
+
code, external calls, App messages and App and Site screenshot reads.
|
|
8
9
|
[docs/families.md](docs/families.md) describes each and the test host.
|
|
9
10
|
|
|
10
|
-
The
|
|
11
|
+
The bridge carries no credential handling and no Runtime family. House builds
|
|
11
12
|
the MD Runtime client into the page beside the bridge, and `bridge.runtime`
|
|
12
13
|
resolves to that client, so the App imports and constructs none.
|
|
13
14
|
|
|
15
|
+
House's `find_command` "App" is the guide to building an App or a Site.
|
|
16
|
+
|
|
14
17
|
## Use
|
|
15
18
|
|
|
16
19
|
```js
|
|
17
|
-
import {
|
|
20
|
+
import { houseBridge } from '@agentshouse/app-bridge';
|
|
18
21
|
|
|
19
|
-
const bridge =
|
|
22
|
+
const bridge = houseBridge();
|
|
20
23
|
const md = await bridge.runtime;
|
|
21
24
|
|
|
22
25
|
bridge.onAddress((address) => render(address));
|
|
@@ -32,22 +35,24 @@ async function manageSecurity(securityPath) {
|
|
|
32
35
|
try {
|
|
33
36
|
await bridge.ceremony(securityPath);
|
|
34
37
|
} catch (refusal) {
|
|
35
|
-
report(refusal);
|
|
38
|
+
if (refusal.name === 'HouseCeremonyRefused') report(refusal.path);
|
|
36
39
|
}
|
|
37
40
|
}
|
|
38
41
|
```
|
|
39
42
|
|
|
40
43
|
`bridge.runtime` resolves to the `MdRuntimeClient` of
|
|
41
|
-
`@agentshouse/mdruntime/client`, whose README describes its members.
|
|
44
|
+
`@agentshouse/mdruntime/client`, whose README describes its members. Its
|
|
45
|
+
`call(name, input)` reaches House: `HouseCalls` names each Call an App makes
|
|
46
|
+
and the input it takes, such as `call('member.list', { room_ref })`.
|
|
42
47
|
|
|
43
48
|
## Test without a House
|
|
44
49
|
|
|
45
50
|
```js
|
|
46
|
-
import {
|
|
51
|
+
import { houseBridge } from '@agentshouse/app-bridge';
|
|
47
52
|
import { createHouseBridgeTestHost } from '@agentshouse/app-bridge/test-host';
|
|
48
53
|
|
|
49
54
|
const host = createHouseBridgeTestHost({ address: '/rooms/atlas' });
|
|
50
|
-
const bridge =
|
|
55
|
+
const bridge = houseBridge();
|
|
51
56
|
|
|
52
57
|
bridge.push('/rooms/atlas/members');
|
|
53
58
|
host.open('/rooms/atlas');
|
package/dist/calls.d.ts
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
export interface HouseCalls {
|
|
2
|
+
readonly 'access.read': {
|
|
3
|
+
readonly room_ref: string;
|
|
4
|
+
};
|
|
5
|
+
readonly 'conversation.answer': {
|
|
6
|
+
readonly answer: {
|
|
7
|
+
readonly interaction_id: string;
|
|
8
|
+
readonly response: unknown;
|
|
9
|
+
};
|
|
10
|
+
readonly conversation: string;
|
|
11
|
+
readonly operation_id?: string;
|
|
12
|
+
};
|
|
13
|
+
readonly 'conversation.create': {
|
|
14
|
+
readonly agent_id: string;
|
|
15
|
+
readonly operation_id?: string;
|
|
16
|
+
};
|
|
17
|
+
readonly 'conversation.delete': {
|
|
18
|
+
readonly conversation: string;
|
|
19
|
+
readonly operation_id?: string;
|
|
20
|
+
};
|
|
21
|
+
readonly 'conversation.history': {
|
|
22
|
+
readonly conversations: readonly {
|
|
23
|
+
readonly after?: string;
|
|
24
|
+
readonly conversation: string;
|
|
25
|
+
}[];
|
|
26
|
+
};
|
|
27
|
+
readonly 'conversation.interrupt': {
|
|
28
|
+
readonly conversation: string;
|
|
29
|
+
readonly operation_id?: string;
|
|
30
|
+
};
|
|
31
|
+
readonly 'conversation.kill': {
|
|
32
|
+
readonly conversation: string;
|
|
33
|
+
readonly operation_id?: string;
|
|
34
|
+
};
|
|
35
|
+
readonly 'conversation.markDone': {
|
|
36
|
+
readonly conversation_id?: string;
|
|
37
|
+
readonly result?: string;
|
|
38
|
+
readonly operation_id: string;
|
|
39
|
+
};
|
|
40
|
+
readonly 'conversation.rename': {
|
|
41
|
+
readonly conversation_id: string;
|
|
42
|
+
readonly operation_id: string;
|
|
43
|
+
readonly name: string;
|
|
44
|
+
};
|
|
45
|
+
readonly 'conversation.send': {
|
|
46
|
+
readonly conversation: string;
|
|
47
|
+
readonly files?: readonly {
|
|
48
|
+
readonly blob: Blob;
|
|
49
|
+
readonly byteLength: number;
|
|
50
|
+
readonly contentHash: string;
|
|
51
|
+
readonly height?: number;
|
|
52
|
+
readonly mediaType: string;
|
|
53
|
+
readonly name: string;
|
|
54
|
+
readonly width?: number;
|
|
55
|
+
}[];
|
|
56
|
+
readonly operation_id?: string;
|
|
57
|
+
readonly text?: string;
|
|
58
|
+
};
|
|
59
|
+
readonly 'conversation.setOption': {
|
|
60
|
+
readonly conversation_id: string;
|
|
61
|
+
readonly operation_id: string;
|
|
62
|
+
readonly option: string;
|
|
63
|
+
readonly value: string | boolean;
|
|
64
|
+
};
|
|
65
|
+
readonly 'feature.list': Readonly<Record<string, never>>;
|
|
66
|
+
readonly 'holder.list': Readonly<Record<string, never>>;
|
|
67
|
+
readonly 'holder.ring': {
|
|
68
|
+
readonly to: string;
|
|
69
|
+
};
|
|
70
|
+
readonly 'member.list': {
|
|
71
|
+
readonly room_ref: string;
|
|
72
|
+
readonly history?: boolean;
|
|
73
|
+
readonly limit?: number;
|
|
74
|
+
readonly cursor?: string;
|
|
75
|
+
};
|
|
76
|
+
readonly 'message.send': {
|
|
77
|
+
readonly to: string;
|
|
78
|
+
readonly body: unknown;
|
|
79
|
+
};
|
|
80
|
+
readonly 'processing.read': {
|
|
81
|
+
readonly room_ref: string;
|
|
82
|
+
readonly path: string;
|
|
83
|
+
};
|
|
84
|
+
readonly 'record.list': {
|
|
85
|
+
readonly room_ref: string;
|
|
86
|
+
readonly record?: string;
|
|
87
|
+
readonly cursor?: string;
|
|
88
|
+
readonly limit?: number;
|
|
89
|
+
readonly order?: 'asc' | 'desc';
|
|
90
|
+
};
|
|
91
|
+
readonly 'relay.read': Readonly<Record<string, never>>;
|
|
92
|
+
readonly 'routine.list': {
|
|
93
|
+
readonly room_ref: string;
|
|
94
|
+
};
|
|
95
|
+
readonly 'routineAccess.list': {
|
|
96
|
+
readonly room_ref: string;
|
|
97
|
+
};
|
|
98
|
+
readonly 'routineAllowance.list': {
|
|
99
|
+
readonly room_ref: string;
|
|
100
|
+
readonly access: string;
|
|
101
|
+
};
|
|
102
|
+
readonly 'routineRequest.list': {
|
|
103
|
+
readonly room_ref: string;
|
|
104
|
+
};
|
|
105
|
+
readonly 'storage.read': {
|
|
106
|
+
readonly room_ref?: string;
|
|
107
|
+
};
|
|
108
|
+
readonly submit: {
|
|
109
|
+
readonly content: string;
|
|
110
|
+
};
|
|
111
|
+
readonly 'trigger.list': {
|
|
112
|
+
readonly room_ref: string;
|
|
113
|
+
};
|
|
114
|
+
readonly 'triggerOccurrence.list': {
|
|
115
|
+
readonly room_ref: string;
|
|
116
|
+
};
|
|
117
|
+
}
|
package/dist/calls.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import type { MdRuntimeClient } from '@agentshouse/mdruntime/client';
|
|
2
|
+
import type { HouseCalls } from './calls.js';
|
|
3
|
+
export type { HouseCalls } from './calls.js';
|
|
4
|
+
type Open<Value extends string> = Value | (string & {});
|
|
5
|
+
export type HouseCallInput<Name extends string> = Name extends keyof HouseCalls ? HouseCalls[Name] : unknown;
|
|
6
|
+
export interface HouseRuntime extends Omit<MdRuntimeClient, 'call'> {
|
|
7
|
+
call<Name extends Open<keyof HouseCalls>>(name: Name, input: HouseCallInput<Name>): ReturnType<MdRuntimeClient['call']>;
|
|
8
|
+
}
|
|
9
|
+
export interface HouseSyncState {
|
|
10
|
+
readonly state: string;
|
|
11
|
+
readonly providerUnreachable: boolean;
|
|
12
|
+
}
|
|
13
|
+
export interface HouseCredentialState {
|
|
14
|
+
readonly state: Open<'none' | 'held' | 'rejected'>;
|
|
15
|
+
readonly expiresAt: number | null;
|
|
16
|
+
}
|
|
17
|
+
export type HouseAnalyticsChoice = 'accept' | 'decline';
|
|
18
|
+
export interface HouseAnalytics {
|
|
19
|
+
readonly measures: boolean;
|
|
20
|
+
readonly choice: Open<HouseAnalyticsChoice> | null;
|
|
21
|
+
}
|
|
22
|
+
export interface HouseStatus {
|
|
23
|
+
readonly app: string | null;
|
|
24
|
+
readonly preferences: {
|
|
25
|
+
readonly roomRef: string;
|
|
26
|
+
readonly path: string;
|
|
27
|
+
} | null;
|
|
28
|
+
readonly runtime: string;
|
|
29
|
+
readonly credential: HouseCredentialState;
|
|
30
|
+
readonly sync: HouseSyncState | null;
|
|
31
|
+
readonly refusal: {
|
|
32
|
+
readonly code: string;
|
|
33
|
+
readonly retryable: boolean;
|
|
34
|
+
} | null;
|
|
35
|
+
readonly loss: string | null;
|
|
36
|
+
readonly analytics: HouseAnalytics;
|
|
37
|
+
readonly serviceMode: Open<'open' | 'write-halted'> | null;
|
|
38
|
+
readonly offline: boolean;
|
|
39
|
+
}
|
|
40
|
+
export type HouseNavigationMode = 'push' | 'replace';
|
|
41
|
+
export interface HouseTab {
|
|
42
|
+
readonly title?: string;
|
|
43
|
+
readonly icon?: string;
|
|
44
|
+
}
|
|
45
|
+
interface HouseRelease {
|
|
46
|
+
readonly app_ref: string;
|
|
47
|
+
readonly release: string;
|
|
48
|
+
}
|
|
49
|
+
export interface HouseSourceTop extends HouseRelease {
|
|
50
|
+
readonly folders: readonly string[];
|
|
51
|
+
readonly files: readonly {
|
|
52
|
+
readonly name: string;
|
|
53
|
+
readonly size: number;
|
|
54
|
+
}[];
|
|
55
|
+
readonly readme: string | null;
|
|
56
|
+
}
|
|
57
|
+
export type HouseSourceFile = {
|
|
58
|
+
readonly path: string;
|
|
59
|
+
readonly size: number;
|
|
60
|
+
} & ({
|
|
61
|
+
readonly text: string;
|
|
62
|
+
} | {
|
|
63
|
+
readonly base64: string;
|
|
64
|
+
});
|
|
65
|
+
export interface HouseSourceDownload extends HouseRelease {
|
|
66
|
+
readonly files: readonly HouseSourceFile[];
|
|
67
|
+
}
|
|
68
|
+
export interface HouseExternalRequest {
|
|
69
|
+
readonly url: string;
|
|
70
|
+
readonly method?: string;
|
|
71
|
+
readonly headers?: Readonly<Record<string, string>>;
|
|
72
|
+
readonly body?: string;
|
|
73
|
+
readonly body_base64?: string;
|
|
74
|
+
readonly account?: string;
|
|
75
|
+
readonly slot?: string;
|
|
76
|
+
}
|
|
77
|
+
export type HouseExternalAnswer = {
|
|
78
|
+
readonly status: number;
|
|
79
|
+
readonly headers: Readonly<Record<string, string | readonly string[]>>;
|
|
80
|
+
} & ({
|
|
81
|
+
readonly body: string;
|
|
82
|
+
} | {
|
|
83
|
+
readonly body_base64: string;
|
|
84
|
+
});
|
|
85
|
+
export interface HouseRefusal {
|
|
86
|
+
readonly error: {
|
|
87
|
+
readonly code: string;
|
|
88
|
+
readonly retryable: boolean;
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
export interface HouseAppMessage {
|
|
92
|
+
readonly from: string;
|
|
93
|
+
readonly body: unknown;
|
|
94
|
+
}
|
|
95
|
+
export type HouseInstallation = Open<'accepted' | 'dismissed' | 'manual'>;
|
|
96
|
+
export type HouseNotifications = Open<'on' | 'off' | 'blocked' | 'unavailable'>;
|
|
97
|
+
export interface HouseCeremonyRefused extends Error {
|
|
98
|
+
readonly name: 'HouseCeremonyRefused';
|
|
99
|
+
readonly code: Open<'house_ceremony_refused'>;
|
|
100
|
+
readonly retryable: boolean;
|
|
101
|
+
readonly path: string | null;
|
|
102
|
+
}
|
|
103
|
+
export interface HouseNavigationRefused extends Error {
|
|
104
|
+
readonly name: 'HouseNavigationRefused';
|
|
105
|
+
readonly code: Open<'house_navigation_refused'>;
|
|
106
|
+
readonly retryable: boolean;
|
|
107
|
+
readonly address: string | null;
|
|
108
|
+
}
|
|
109
|
+
export interface HouseBrowserRefused extends Error {
|
|
110
|
+
readonly code: string;
|
|
111
|
+
readonly retryable: boolean;
|
|
112
|
+
}
|
|
113
|
+
export interface HouseBridge {
|
|
114
|
+
readonly runtime: Promise<HouseRuntime>;
|
|
115
|
+
address(): string | null;
|
|
116
|
+
onAddress(listener: (address: string) => void): () => void;
|
|
117
|
+
push(address: string): Promise<string>;
|
|
118
|
+
replace(address: string): Promise<string>;
|
|
119
|
+
ceremony(path: string): Promise<never>;
|
|
120
|
+
install(): Promise<HouseInstallation>;
|
|
121
|
+
notifications(on?: boolean): Promise<HouseNotifications>;
|
|
122
|
+
setBadge(unread: readonly string[]): Promise<void>;
|
|
123
|
+
setTab(tab: HouseTab): void;
|
|
124
|
+
sourceTop(app: string, release?: string): Promise<HouseSourceTop | HouseRefusal | null>;
|
|
125
|
+
sourceDownload(app: string, release?: string): Promise<HouseSourceDownload | HouseRefusal | null>;
|
|
126
|
+
callExternal(request: HouseExternalRequest): Promise<HouseExternalAnswer | HouseRefusal | null>;
|
|
127
|
+
readScreenshot(app: string): Promise<Blob | null>;
|
|
128
|
+
readSiteScreenshot(site: string): Promise<Blob | null>;
|
|
129
|
+
status(): HouseStatus | null;
|
|
130
|
+
onStatus(listener: (status: HouseStatus) => void): () => void;
|
|
131
|
+
messages(listener: (message: HouseAppMessage) => void): () => void;
|
|
132
|
+
analyticsConsent(choice: HouseAnalyticsChoice): void;
|
|
133
|
+
}
|
|
134
|
+
export interface HouseHeld {
|
|
135
|
+
readonly houseBridge: HouseBridge;
|
|
136
|
+
}
|
|
137
|
+
export declare const houseBridge: () => HouseBridge;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const houseBridge = () => globalThis.houseBridge;
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import type { MdRuntimeClient } from '@agentshouse/mdruntime/client';
|
|
2
|
-
import type {
|
|
3
|
-
import type { HouseAppMessage, HouseExternalAnswer, HouseExternalRequest, HouseGaChoice, HouseNavigationMode, HouseRefusal, HouseSourceDownload, HouseSourceTop, HouseStatus, HouseTab } from './wire.js';
|
|
2
|
+
import type { HouseAnalyticsChoice, HouseAppMessage, HouseExternalAnswer, HouseExternalRequest, HouseNavigationMode, HouseRefusal, HouseSourceDownload, HouseSourceTop, HouseStatus, HouseTab } from './index.js';
|
|
4
3
|
export interface HouseBridgeNavigation {
|
|
5
4
|
readonly mode: HouseNavigationMode;
|
|
6
5
|
readonly address: string;
|
|
7
6
|
}
|
|
8
|
-
export interface HouseBridgeTestHost
|
|
7
|
+
export interface HouseBridgeTestHost {
|
|
9
8
|
readonly navigations: readonly HouseBridgeNavigation[];
|
|
10
9
|
readonly ceremonies: readonly string[];
|
|
11
|
-
readonly consents: readonly
|
|
10
|
+
readonly consents: readonly HouseAnalyticsChoice[];
|
|
12
11
|
readonly tabs: readonly HouseTab[];
|
|
13
12
|
readonly externalCalls: readonly HouseExternalRequest[];
|
|
14
13
|
address(): string;
|
|
@@ -20,9 +19,6 @@ export interface HouseBridgeTestHost extends HouseBridgeSource {
|
|
|
20
19
|
export interface HouseBridgeTestHostOptions {
|
|
21
20
|
readonly address?: string;
|
|
22
21
|
readonly status?: HouseStatus;
|
|
23
|
-
readonly images?: readonly (HouseImageCopy & {
|
|
24
|
-
readonly room: string;
|
|
25
|
-
})[];
|
|
26
22
|
readonly sources?: readonly {
|
|
27
23
|
readonly top: HouseSourceTop;
|
|
28
24
|
readonly download: HouseSourceDownload;
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
const HOST_ORIGIN = 'https://house.invalid';
|
|
2
|
+
const ADDRESS_MAX_CHARS = 64 * 1024;
|
|
3
|
+
function sameOriginPath(value) {
|
|
4
|
+
if (typeof value !== 'string')
|
|
5
|
+
return null;
|
|
6
|
+
if (value.charAt(0) !== '/' || value.slice(0, 2) === '//')
|
|
7
|
+
return null;
|
|
8
|
+
if (value.length > ADDRESS_MAX_CHARS)
|
|
9
|
+
return null;
|
|
10
|
+
const target = new URL(value, HOST_ORIGIN);
|
|
11
|
+
return target.origin === HOST_ORIGIN ? `${target.pathname}${target.search}${target.hash}` : null;
|
|
12
|
+
}
|
|
13
|
+
function appAddress(address) {
|
|
14
|
+
return address.startsWith('/') ? address : `/${address}`;
|
|
15
|
+
}
|
|
16
|
+
function ceremonyRefused(path) {
|
|
17
|
+
return Object.assign(new Error('house_ceremony_refused'), {
|
|
18
|
+
name: 'HouseCeremonyRefused',
|
|
19
|
+
code: 'house_ceremony_refused',
|
|
20
|
+
retryable: false,
|
|
21
|
+
path,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
function navigationRefused(address) {
|
|
25
|
+
return Object.assign(new Error('house_navigation_refused'), {
|
|
26
|
+
name: 'HouseNavigationRefused',
|
|
27
|
+
code: 'house_navigation_refused',
|
|
28
|
+
retryable: false,
|
|
29
|
+
address,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
function followHeld(listeners, listener, held) {
|
|
33
|
+
let following = true;
|
|
34
|
+
queueMicrotask(() => {
|
|
35
|
+
if (!following)
|
|
36
|
+
return;
|
|
37
|
+
listeners.add(listener);
|
|
38
|
+
const value = held();
|
|
39
|
+
if (value !== null)
|
|
40
|
+
listener(value);
|
|
41
|
+
});
|
|
42
|
+
return () => {
|
|
43
|
+
following = false;
|
|
44
|
+
listeners.delete(listener);
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
export function createHouseBridgeTestHost(options = {}) {
|
|
48
|
+
const navigations = [];
|
|
49
|
+
const ceremonies = [];
|
|
50
|
+
const consents = [];
|
|
51
|
+
const tabs = [];
|
|
52
|
+
const externalCalls = [];
|
|
53
|
+
const addressListeners = new Set();
|
|
54
|
+
const statusListeners = new Set();
|
|
55
|
+
const messageListeners = new Set();
|
|
56
|
+
let current = appAddress(options.address ?? '/');
|
|
57
|
+
let published = options.status ?? null;
|
|
58
|
+
let shown = null;
|
|
59
|
+
let seen = null;
|
|
60
|
+
let attached = () => { };
|
|
61
|
+
const runtime = new Promise((resolve) => {
|
|
62
|
+
attached = resolve;
|
|
63
|
+
});
|
|
64
|
+
function arrive(target) {
|
|
65
|
+
current = target;
|
|
66
|
+
queueMicrotask(() => {
|
|
67
|
+
shown = target;
|
|
68
|
+
for (const listener of addressListeners)
|
|
69
|
+
listener(target);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
function publish(next) {
|
|
73
|
+
published = next;
|
|
74
|
+
queueMicrotask(() => {
|
|
75
|
+
seen = next;
|
|
76
|
+
for (const listener of statusListeners)
|
|
77
|
+
listener(next);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
function navigate(mode, asked) {
|
|
81
|
+
let path = asked;
|
|
82
|
+
if (/^https?:\/\//i.test(asked)) {
|
|
83
|
+
try {
|
|
84
|
+
const target = new URL(asked);
|
|
85
|
+
path = target.origin === HOST_ORIGIN ? target.pathname + target.search + target.hash : '';
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
path = '';
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const target = sameOriginPath(path);
|
|
92
|
+
if (target === null)
|
|
93
|
+
return Promise.reject(navigationRefused(asked));
|
|
94
|
+
navigations.push({ mode, address: target });
|
|
95
|
+
arrive(target);
|
|
96
|
+
return new Promise((resolve) => {
|
|
97
|
+
queueMicrotask(() => resolve(target));
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
const bridge = {
|
|
101
|
+
runtime,
|
|
102
|
+
address: () => shown,
|
|
103
|
+
onAddress(listener) {
|
|
104
|
+
return followHeld(addressListeners, listener, () => shown);
|
|
105
|
+
},
|
|
106
|
+
push: (asked) => navigate('push', asked),
|
|
107
|
+
replace: (asked) => navigate('replace', asked),
|
|
108
|
+
ceremony(path) {
|
|
109
|
+
const target = sameOriginPath(path);
|
|
110
|
+
if (target === null)
|
|
111
|
+
return Promise.reject(ceremonyRefused(path));
|
|
112
|
+
ceremonies.push(target);
|
|
113
|
+
return new Promise(() => { });
|
|
114
|
+
},
|
|
115
|
+
install: () => Promise.resolve('manual'),
|
|
116
|
+
notifications: () => Promise.resolve('unavailable'),
|
|
117
|
+
setBadge: () => Promise.resolve(),
|
|
118
|
+
setTab(tab) {
|
|
119
|
+
tabs.push(tab);
|
|
120
|
+
},
|
|
121
|
+
sourceTop: (app) => Promise.resolve(options.sources?.find((entry) => entry.top.app_ref === app)?.top ?? null),
|
|
122
|
+
sourceDownload: (app) => Promise.resolve(options.sources?.find((entry) => entry.top.app_ref === app)?.download ?? null),
|
|
123
|
+
callExternal(request) {
|
|
124
|
+
externalCalls.push(request);
|
|
125
|
+
return Promise.resolve(options.external?.(request) ?? null);
|
|
126
|
+
},
|
|
127
|
+
readScreenshot: (app) => Promise.resolve(options.screenshots?.[app] ?? null),
|
|
128
|
+
readSiteScreenshot: (site) => Promise.resolve(options.siteScreenshots?.[site] ?? null),
|
|
129
|
+
status: () => seen,
|
|
130
|
+
onStatus(listener) {
|
|
131
|
+
return followHeld(statusListeners, listener, () => seen);
|
|
132
|
+
},
|
|
133
|
+
messages(listener) {
|
|
134
|
+
messageListeners.add(listener);
|
|
135
|
+
return () => {
|
|
136
|
+
messageListeners.delete(listener);
|
|
137
|
+
};
|
|
138
|
+
},
|
|
139
|
+
analyticsConsent(choice) {
|
|
140
|
+
if (published === null || !published.analytics.measures)
|
|
141
|
+
return;
|
|
142
|
+
if (choice !== 'accept' && choice !== 'decline')
|
|
143
|
+
return;
|
|
144
|
+
consents.push(choice);
|
|
145
|
+
publish({ ...published, analytics: { measures: true, choice } });
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
Object.defineProperty(globalThis, 'houseBridge', { value: bridge, configurable: true });
|
|
149
|
+
if (published !== null)
|
|
150
|
+
publish(published);
|
|
151
|
+
arrive(current);
|
|
152
|
+
return {
|
|
153
|
+
navigations,
|
|
154
|
+
ceremonies,
|
|
155
|
+
consents,
|
|
156
|
+
tabs,
|
|
157
|
+
externalCalls,
|
|
158
|
+
address: () => current,
|
|
159
|
+
open(asked) {
|
|
160
|
+
arrive(appAddress(asked));
|
|
161
|
+
},
|
|
162
|
+
publish,
|
|
163
|
+
message({ from, body }) {
|
|
164
|
+
queueMicrotask(() => {
|
|
165
|
+
for (const listener of messageListeners)
|
|
166
|
+
listener({ from, body });
|
|
167
|
+
});
|
|
168
|
+
},
|
|
169
|
+
attachRuntime(client) {
|
|
170
|
+
attached(client);
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
}
|
package/docs/families.md
CHANGED
|
@@ -18,28 +18,32 @@
|
|
|
18
18
|
`'accepted'` or `'dismissed'` with the User's choice, spending that prompt;
|
|
19
19
|
otherwise, as on every iPhone and iPad, it resolves `'manual'` and the App
|
|
20
20
|
shows its own instructions. A prompt the browser refuses to show, as for a
|
|
21
|
-
call made without a click, rejects with
|
|
22
|
-
and stays ready. Call it from a click. The standard
|
|
21
|
+
call made without a click, rejects with an error named and coded by the
|
|
22
|
+
browser's own error name, with `retryable` false, and stays ready. Call it from a click. The standard
|
|
23
23
|
`display-mode: standalone` media query tells the App it runs installed.
|
|
24
24
|
- **Notifications** - House's own panel alone; an App's or a Site's call
|
|
25
|
-
rejects with `HouseCeremonyRefused`. `notifications()` answers this browser
|
|
25
|
+
rejects with an error named `HouseCeremonyRefused`, carrying `code` and `retryable`. `notifications()` answers this browser
|
|
26
26
|
storage's push state: `'on'`, `'off'`, `'blocked'` where the browser refuses
|
|
27
27
|
notifications, or `'unavailable'` where it has no push, as on an iPhone or
|
|
28
28
|
iPad outside a Home-screen app. `notifications(true)` asks the browser's
|
|
29
29
|
permission and subscribes, `notifications(false)` unsubscribes, and each
|
|
30
|
-
answers the state it leaves; a failure of the browser's push service
|
|
31
|
-
|
|
30
|
+
answers the state it leaves; a failure of the browser's push service, and
|
|
31
|
+
of `setBadge`, rejects the same way. Call `notifications(true)` from a
|
|
32
32
|
click. `setBadge(unread)` takes the identifiers of the Human Notifications
|
|
33
33
|
unread in this storage and sets the icon's badge to their number, or clears
|
|
34
34
|
it at none, where the browser shows one.
|
|
35
|
-
- **Status** - the App reference the runtime client is attached under,
|
|
36
|
-
|
|
35
|
+
- **Status** - the App reference the runtime client is attached under,
|
|
36
|
+
`preferences`: the `roomRef` and Path of the App's own preferences document,
|
|
37
|
+
or `null` until the runtime holds that Room, sync
|
|
38
|
+
state, credential state and its `expiresAt`, observed failure, `analytics`:
|
|
37
39
|
whether the page measures and the visitor's stored analytics choice, and
|
|
38
|
-
`
|
|
40
|
+
`serviceMode`: `open`, `write-halted` while House refuses writes, so the
|
|
39
41
|
App can stop retrying them, or `null` while the runtime holds no House
|
|
40
|
-
connection.
|
|
41
|
-
|
|
42
|
-
|
|
42
|
+
connection. Every status field is camelCase. `onStatus(listener)` and
|
|
43
|
+
`onAddress(listener)` hand a listener the status and the address the bridge
|
|
44
|
+
already holds, then each change.
|
|
45
|
+
The App reports the visitor's choice with `analyticsConsent('accept')` or
|
|
46
|
+
`analyticsConsent('decline')`; the shell hands it to House's analytics loader, and
|
|
43
47
|
the choice never reaches House.
|
|
44
48
|
- **Site image read** - given a Published Room and an original attachment
|
|
45
49
|
reference, it answers the web copy's path and metadata, or `null` when no
|
|
@@ -51,14 +55,14 @@
|
|
|
51
55
|
path, its size and its `text`, or its bytes as `base64`. The browser keeps
|
|
52
56
|
each answer per App and release: a read naming a release it holds answers
|
|
53
57
|
without asking House, and keeping a newer release drops the earlier one. A
|
|
54
|
-
refusal is House's `{ error: { code } }`, `app_release_not_found` for an
|
|
58
|
+
refusal is House's `{ error: { code, retryable } }`, `app_release_not_found` for an
|
|
55
59
|
App with no release; a read House does not answer, or one that outlived
|
|
56
60
|
the browser's signed-in User, is `null`.
|
|
57
61
|
- **External calls** - `callExternal({ url, method, headers, body, slot })`
|
|
58
62
|
has House's server send an HTTPS request to one of the release's declared
|
|
59
63
|
external origins and resolves with the service's `status`, `headers` and its
|
|
60
64
|
body as `body` text or `body_base64`, or a refusal as House's
|
|
61
|
-
`{ error: { code } }`, or `null` when House does not answer. House fills in the
|
|
65
|
+
`{ error: { code, retryable } }`, or `null` when House does not answer. House fills in the
|
|
62
66
|
App and its running release; naming an account slot places the key of the
|
|
63
67
|
account the User connected to it, which the App never sees, and naming none
|
|
64
68
|
sends no key. A Site's call names no slot, and the House panel names an
|
|
@@ -67,7 +71,7 @@
|
|
|
67
71
|
another holder of this App sends to this User's running copies, as `from`,
|
|
68
72
|
the sender's User id, and `body`, the JSON value they sent, until the
|
|
69
73
|
function it returns is called. App code sends one through the Runtime's
|
|
70
|
-
`call('
|
|
74
|
+
`call('message.send', { to, body })` with the recipient's User id and
|
|
71
75
|
a body of at most 32 KiB of JSON; House fills in the App and relays it only
|
|
72
76
|
to copies running now, storing nothing. The panel sends none.
|
|
73
77
|
- **App screenshot read** - given an App reference, `readScreenshot(app)`
|
|
@@ -82,23 +86,24 @@
|
|
|
82
86
|
|
|
83
87
|
## The test host
|
|
84
88
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
`
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
`
|
|
97
|
-
`
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
89
|
+
`createHouseBridgeTestHost(options)` plays House in one page: it installs a
|
|
90
|
+
bridge object that `houseBridge()` returns, until the next host replaces it.
|
|
91
|
+
It delivers the address the App starts at and each address `open` plays as
|
|
92
|
+
Back or Forward, admitting nothing, so it delivers `//notes` exactly as a
|
|
93
|
+
browser at `/apps/<app>//notes` does. It records every push and replace and
|
|
94
|
+
answers each with the address the shell would then show, records an admitted
|
|
95
|
+
ceremony, and refuses a push, replace or ceremony that is not a same-origin
|
|
96
|
+
absolute path of at most 1,000 characters with an error named
|
|
97
|
+
`HouseNavigationRefused` or `HouseCeremonyRefused`. It publishes a status,
|
|
98
|
+
its `serviceMode` included, records in `consents` each `analyticsConsent` it
|
|
99
|
+
takes while that status measures and publishes the status the choice leaves,
|
|
100
|
+
records in `tabs` each `setTab`, answers a release code read from the
|
|
101
|
+
`sources` it was given, or `null`, answers each `callExternal` from the
|
|
102
|
+
`external` function it was given, or `null`, and records the request in
|
|
103
|
+
`externalCalls`, answers `readScreenshot` from the `screenshots` it was given
|
|
104
|
+
by App reference and `readSiteScreenshot` from the `siteScreenshots` it was
|
|
105
|
+
given by Site identifier, answers every `install()` `'manual'`, as a browser
|
|
106
|
+
with no install prompt does, answers every `notifications()` `'unavailable'`,
|
|
107
|
+
as a browser without push does, settles every `setBadge`, delivers each App
|
|
108
|
+
message `message({ from, body })` plays, and hands `bridge.runtime` the MD
|
|
109
|
+
Runtime client `attachRuntime(client)` gives it.
|