@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 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";
@@ -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
- export { isHome } from "./protocol.js";
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.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",