@tabs-and-spaces/sdk 0.3.0 → 0.5.0
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/client.d.ts +10 -0
- package/dist/client.js +15 -0
- package/dist/index.d.ts +1 -1
- package/dist/protocol.d.ts +21 -10
- package/dist/protocol.js +12 -1
- package/dist/server.d.ts +100 -1
- package/dist/server.js +3 -2
- package/package.json +1 -1
package/dist/client.d.ts
CHANGED
|
@@ -38,6 +38,11 @@ export interface Connection {
|
|
|
38
38
|
readonly context: PageContext;
|
|
39
39
|
/** The workspace's theme now. */
|
|
40
40
|
readonly theme: Theme;
|
|
41
|
+
/**
|
|
42
|
+
* The workspace is read-only now (its bill is unpaid): your data can be read,
|
|
43
|
+
* not changed. Kept in step with `context.workspace.readOnly`.
|
|
44
|
+
*/
|
|
45
|
+
readonly readOnly: boolean;
|
|
41
46
|
/**
|
|
42
47
|
* A token for your server: who's looking, signed by the workspace. Fresh
|
|
43
48
|
* (a new one is fetched when this one has less than a minute left).
|
|
@@ -77,6 +82,11 @@ export interface Connection {
|
|
|
77
82
|
}): Promise<void>;
|
|
78
83
|
/** Called when the reader switches between light and dark. Returns an unsubscribe. */
|
|
79
84
|
onTheme(listener: (theme: Theme) => void): () => void;
|
|
85
|
+
/**
|
|
86
|
+
* Called when the workspace becomes read-only, or writable again (its bill
|
|
87
|
+
* paid), while your page is open. Returns an unsubscribe.
|
|
88
|
+
*/
|
|
89
|
+
onReadOnly(listener: (readOnly: boolean) => void): () => void;
|
|
80
90
|
/** Stops listening. */
|
|
81
91
|
close(): void;
|
|
82
92
|
}
|
package/dist/client.js
CHANGED
|
@@ -34,6 +34,7 @@ export function connect({ timeoutMs = 10_000, window: win = window } = {}) {
|
|
|
34
34
|
let theme = "light";
|
|
35
35
|
let counter = 0;
|
|
36
36
|
const themeListeners = new Set();
|
|
37
|
+
const readOnlyListeners = new Set();
|
|
37
38
|
const waiting = new Map();
|
|
38
39
|
let pending = null;
|
|
39
40
|
const send = (message) => parent.postMessage(message, hostOrigin ?? "*");
|
|
@@ -85,6 +86,13 @@ export function connect({ timeoutMs = 10_000, window: win = window } = {}) {
|
|
|
85
86
|
waiter.reject(new WorkspaceError(message.message, message.code ?? null));
|
|
86
87
|
return;
|
|
87
88
|
}
|
|
89
|
+
case "readOnly":
|
|
90
|
+
if (!context || context.workspace.readOnly === message.readOnly)
|
|
91
|
+
return;
|
|
92
|
+
context = { ...context, workspace: { ...context.workspace, readOnly: message.readOnly } };
|
|
93
|
+
for (const listener of readOnlyListeners)
|
|
94
|
+
listener(message.readOnly);
|
|
95
|
+
return;
|
|
88
96
|
case "theme":
|
|
89
97
|
theme = message.theme;
|
|
90
98
|
for (const listener of themeListeners)
|
|
@@ -105,6 +113,9 @@ export function connect({ timeoutMs = 10_000, window: win = window } = {}) {
|
|
|
105
113
|
get context() {
|
|
106
114
|
return context;
|
|
107
115
|
},
|
|
116
|
+
get readOnly() {
|
|
117
|
+
return context.workspace.readOnly;
|
|
118
|
+
},
|
|
108
119
|
get theme() {
|
|
109
120
|
return theme;
|
|
110
121
|
},
|
|
@@ -164,6 +175,10 @@ export function connect({ timeoutMs = 10_000, window: win = window } = {}) {
|
|
|
164
175
|
},
|
|
165
176
|
};
|
|
166
177
|
},
|
|
178
|
+
onReadOnly(listener) {
|
|
179
|
+
readOnlyListeners.add(listener);
|
|
180
|
+
return () => readOnlyListeners.delete(listener);
|
|
181
|
+
},
|
|
167
182
|
onTheme(listener) {
|
|
168
183
|
themeListeners.add(listener);
|
|
169
184
|
return () => themeListeners.delete(listener);
|
package/dist/index.d.ts
CHANGED
|
@@ -3,4 +3,4 @@
|
|
|
3
3
|
* server: `verifyToken()` from "@tabs-and-spaces/sdk/server".
|
|
4
4
|
*/
|
|
5
5
|
export { connect, WorkspaceError, type Collection, type Connection, type ConnectOptions, type Values, } from "./client.js";
|
|
6
|
-
export { PROTOCOL_VERSION, readAppMessage, readHostMessage, type AppMessage, type DataRecord, type DataRequest, type DataScope, type EventKey, type HomeContext, type HostMessage, isHome, type PageContext, type TabContext, type Theme, } from "./protocol.js";
|
|
6
|
+
export { PROTOCOL_VERSION, readAppMessage, readHostMessage, type AppMessage, type DataRecord, type DataRequest, type DataScope, type EventKey, type HomeContext, type HostMessage, isHome, type PageContext, type TabContext, type Theme, type WorkspaceInfo, } from "./protocol.js";
|
package/dist/protocol.d.ts
CHANGED
|
@@ -5,17 +5,24 @@
|
|
|
5
5
|
* tokens and theme changes. Every message carries `tands: 1`.
|
|
6
6
|
*/
|
|
7
7
|
export declare const PROTOCOL_VERSION = 1;
|
|
8
|
+
/**
|
|
9
|
+
* The workspace around your page. `readOnly`: it's read-only until its bill is
|
|
10
|
+
* paid, so your app's data can be read but not changed (writes are refused
|
|
11
|
+
* with `WORKSPACE_READ_ONLY`); say so, and offer nothing that writes.
|
|
12
|
+
*/
|
|
13
|
+
export interface WorkspaceInfo {
|
|
14
|
+
id: string;
|
|
15
|
+
slug: string;
|
|
16
|
+
name: string;
|
|
17
|
+
readOnly: boolean;
|
|
18
|
+
}
|
|
8
19
|
/** Who's looking, and where, in a Custom tab (also inside the token, signed). */
|
|
9
20
|
export interface TabContext {
|
|
10
21
|
user: {
|
|
11
22
|
id: string;
|
|
12
23
|
name: string;
|
|
13
24
|
};
|
|
14
|
-
workspace:
|
|
15
|
-
id: string;
|
|
16
|
-
slug: string;
|
|
17
|
-
name: string;
|
|
18
|
-
};
|
|
25
|
+
workspace: WorkspaceInfo;
|
|
19
26
|
/**
|
|
20
27
|
* `name` is the one the reader sees: General and My Space in their language,
|
|
21
28
|
* a DM by its other people.
|
|
@@ -47,11 +54,7 @@ export interface HomeContext {
|
|
|
47
54
|
name: string;
|
|
48
55
|
guest: boolean;
|
|
49
56
|
};
|
|
50
|
-
workspace:
|
|
51
|
-
id: string;
|
|
52
|
-
slug: string;
|
|
53
|
-
name: string;
|
|
54
|
-
};
|
|
57
|
+
workspace: WorkspaceInfo;
|
|
55
58
|
space: null;
|
|
56
59
|
tab: null;
|
|
57
60
|
/** Which of your manifest's `home` pages this is. */
|
|
@@ -213,8 +216,16 @@ export type HostMessage = {
|
|
|
213
216
|
tands: 1;
|
|
214
217
|
type: "theme";
|
|
215
218
|
theme: Theme;
|
|
219
|
+
}
|
|
220
|
+
/** The workspace became read-only, or writable again, while your page is open. */
|
|
221
|
+
| {
|
|
222
|
+
tands: 1;
|
|
223
|
+
type: "readOnly";
|
|
224
|
+
readOnly: boolean;
|
|
216
225
|
};
|
|
217
226
|
/** A message from an app's page, or null. */
|
|
218
227
|
export declare function readAppMessage(data: unknown): AppMessage | null;
|
|
228
|
+
/** A context from a workspace that doesn't say `readOnly` (older ones): writable. */
|
|
229
|
+
export declare function withReadOnly<T extends PageContext>(context: T): T;
|
|
219
230
|
/** A message from the workspace, or null. */
|
|
220
231
|
export declare function readHostMessage(data: unknown): HostMessage | null;
|
package/dist/protocol.js
CHANGED
|
@@ -70,6 +70,13 @@ export function readAppMessage(data) {
|
|
|
70
70
|
}
|
|
71
71
|
return null;
|
|
72
72
|
}
|
|
73
|
+
/** A context from a workspace that doesn't say `readOnly` (older ones): writable. */
|
|
74
|
+
export function withReadOnly(context) {
|
|
75
|
+
const workspace = context.workspace;
|
|
76
|
+
if (!workspace || typeof workspace.readOnly === "boolean")
|
|
77
|
+
return context;
|
|
78
|
+
return { ...context, workspace: { ...workspace, readOnly: false } };
|
|
79
|
+
}
|
|
73
80
|
/** A message from the workspace, or null. */
|
|
74
81
|
export function readHostMessage(data) {
|
|
75
82
|
if (!isRecord(data) || data.tands !== PROTOCOL_VERSION)
|
|
@@ -83,7 +90,7 @@ export function readHostMessage(data) {
|
|
|
83
90
|
? {
|
|
84
91
|
tands: 1,
|
|
85
92
|
type: "context",
|
|
86
|
-
context: data.context,
|
|
93
|
+
context: withReadOnly(data.context),
|
|
87
94
|
token: data.token,
|
|
88
95
|
expiresAt: data.expiresAt,
|
|
89
96
|
theme,
|
|
@@ -111,6 +118,10 @@ export function readHostMessage(data) {
|
|
|
111
118
|
: null;
|
|
112
119
|
case "theme":
|
|
113
120
|
return { tands: 1, type: "theme", theme };
|
|
121
|
+
case "readOnly":
|
|
122
|
+
return typeof data.readOnly === "boolean"
|
|
123
|
+
? { tands: 1, type: "readOnly", readOnly: data.readOnly }
|
|
124
|
+
: null;
|
|
114
125
|
default:
|
|
115
126
|
return null;
|
|
116
127
|
}
|
package/dist/server.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { DataRecord, DataRequest, DataScope, EventKey, PageContext, TabContext } from "./protocol.js";
|
|
2
|
-
export { isHome, type HomeContext, type PageContext, type TabContext } from "./protocol.js";
|
|
2
|
+
export { isHome, type HomeContext, type PageContext, type TabContext, type WorkspaceInfo, } from "./protocol.js";
|
|
3
3
|
/**
|
|
4
4
|
* Checking a Custom tab's token on your server (Node 22+, Deno, Bun, Workers:
|
|
5
5
|
* anywhere with Web Crypto's Ed25519). The workspace signs it (EdDSA); its
|
|
@@ -54,6 +54,105 @@ export interface ActionRequest<Input = Record<string, unknown>> {
|
|
|
54
54
|
dataKey: string;
|
|
55
55
|
dataUrl: string;
|
|
56
56
|
}
|
|
57
|
+
type Tone = "good" | "warning" | "bad" | "neutral";
|
|
58
|
+
/** A day (`2030-01-08`, all day) or an ISO time; without an offset, the asker's. */
|
|
59
|
+
type When = string;
|
|
60
|
+
/**
|
|
61
|
+
* A card showing what an action found, drawn under the agent's answer (the
|
|
62
|
+
* agent then doesn't retype it). Links are http(s) only. At most 8 per answer.
|
|
63
|
+
*/
|
|
64
|
+
export type ActionCard = {
|
|
65
|
+
type: "emails";
|
|
66
|
+
title?: string;
|
|
67
|
+
items: {
|
|
68
|
+
from: string;
|
|
69
|
+
subject: string;
|
|
70
|
+
snippet?: string;
|
|
71
|
+
at?: When;
|
|
72
|
+
url?: string;
|
|
73
|
+
unread?: boolean;
|
|
74
|
+
important?: boolean;
|
|
75
|
+
note?: string;
|
|
76
|
+
}[];
|
|
77
|
+
} | {
|
|
78
|
+
type: "events";
|
|
79
|
+
title?: string;
|
|
80
|
+
items: {
|
|
81
|
+
title: string;
|
|
82
|
+
start: When;
|
|
83
|
+
end?: When;
|
|
84
|
+
where?: string;
|
|
85
|
+
people?: string[];
|
|
86
|
+
url?: string;
|
|
87
|
+
/** A video call's link. */
|
|
88
|
+
join?: string;
|
|
89
|
+
status?: "accepted" | "tentative" | "declined" | "needs_answer";
|
|
90
|
+
note?: string;
|
|
91
|
+
}[];
|
|
92
|
+
} | {
|
|
93
|
+
type: "metrics";
|
|
94
|
+
title?: string;
|
|
95
|
+
items: {
|
|
96
|
+
label: string;
|
|
97
|
+
value: string;
|
|
98
|
+
detail?: string;
|
|
99
|
+
tone?: Tone;
|
|
100
|
+
}[];
|
|
101
|
+
} | {
|
|
102
|
+
type: "table";
|
|
103
|
+
title?: string;
|
|
104
|
+
columns: string[];
|
|
105
|
+
rows: (string | number | null)[][];
|
|
106
|
+
} | {
|
|
107
|
+
type: "list";
|
|
108
|
+
title?: string;
|
|
109
|
+
items: {
|
|
110
|
+
title: string;
|
|
111
|
+
detail?: string;
|
|
112
|
+
meta?: string;
|
|
113
|
+
url?: string;
|
|
114
|
+
tone?: Tone;
|
|
115
|
+
}[];
|
|
116
|
+
} | {
|
|
117
|
+
type: "links";
|
|
118
|
+
title?: string;
|
|
119
|
+
items: {
|
|
120
|
+
label: string;
|
|
121
|
+
url: string;
|
|
122
|
+
detail?: string;
|
|
123
|
+
}[];
|
|
124
|
+
} | {
|
|
125
|
+
type: "note";
|
|
126
|
+
tone?: "info" | "success" | "warning" | "danger";
|
|
127
|
+
title?: string;
|
|
128
|
+
text: string;
|
|
129
|
+
} | {
|
|
130
|
+
type: "bars";
|
|
131
|
+
title?: string;
|
|
132
|
+
unit?: string;
|
|
133
|
+
items: {
|
|
134
|
+
label: string;
|
|
135
|
+
value: number;
|
|
136
|
+
}[];
|
|
137
|
+
} | {
|
|
138
|
+
type: "actions";
|
|
139
|
+
items: {
|
|
140
|
+
label: string;
|
|
141
|
+
prompt: string;
|
|
142
|
+
}[];
|
|
143
|
+
};
|
|
144
|
+
/** What an action's endpoint answers, with 200. */
|
|
145
|
+
export interface ActionAnswer {
|
|
146
|
+
/** For the agent to read: a sentence, or any JSON. */
|
|
147
|
+
result: unknown;
|
|
148
|
+
/** What it found, shown as cards under the agent's answer (actions that change nothing). */
|
|
149
|
+
cards?: ActionCard[];
|
|
150
|
+
/**
|
|
151
|
+
* For your action's view (its manifest's `view`), not the agent: what the
|
|
152
|
+
* page is told besides `result`, as MCP Apps' `structuredContent`.
|
|
153
|
+
*/
|
|
154
|
+
data?: Record<string, unknown>;
|
|
155
|
+
}
|
|
57
156
|
/**
|
|
58
157
|
* Your app's data in the workspace, from your server, with the data key an
|
|
59
158
|
* action was given: the same place (its space, tab and person) as the action,
|
package/dist/server.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
import { withReadOnly } from "./protocol.js";
|
|
2
|
+
export { isHome, } from "./protocol.js";
|
|
2
3
|
export class TokenError extends Error {
|
|
3
4
|
}
|
|
4
5
|
/** The deployment's public keys (cache them; they change only if its secret does). */
|
|
@@ -51,7 +52,7 @@ export async function verifyToken(token, { keys, audience, issuer, now = Date.no
|
|
|
51
52
|
throw new TokenError("It has expired.");
|
|
52
53
|
}
|
|
53
54
|
return {
|
|
54
|
-
context: payload.ctx,
|
|
55
|
+
context: withReadOnly(payload.ctx),
|
|
55
56
|
issuer: String(payload.iss),
|
|
56
57
|
expiresAt: new Date(payload.exp * 1000),
|
|
57
58
|
action: typeof payload.act === "string" ? payload.act : null,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tabs-and-spaces/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Building apps for Tabs & Spaces: a Custom tab's page talks to the workspace, its server checks who's looking. No dependencies.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Tabs & Spaces",
|