@tabs-and-spaces/sdk 0.1.0 → 0.3.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
@@ -1,4 +1,4 @@
1
- import { type DataRecord, type DataRequest, type DataScope, type TabContext, type Theme } from "./protocol.js";
1
+ import { type DataRecord, type DataRequest, type DataScope, type EventKey, type PageContext, type Theme } from "./protocol.js";
2
2
  /** A refusal from the workspace, with its reason's code (like `INVALID` or `FORBIDDEN`). */
3
3
  export declare class WorkspaceError extends Error {
4
4
  readonly code: string | null;
@@ -35,7 +35,7 @@ export interface Collection<T extends Record<string, unknown> = Record<string, u
35
35
  }
36
36
  export interface Connection {
37
37
  /** Who's looking, and where. */
38
- readonly context: TabContext;
38
+ readonly context: PageContext;
39
39
  /** The workspace's theme now. */
40
40
  readonly theme: Theme;
41
41
  /**
@@ -54,6 +54,27 @@ export interface Connection {
54
54
  * (triggers in this space may start a task). Resolves once it's taken.
55
55
  */
56
56
  emit(name: string, data?: Record<string, unknown>): Promise<void>;
57
+ /**
58
+ * Asks for a key your server can report events with, in this tab, whenever they
59
+ * happen (a webhook from another service, say). The person sees what's asked and
60
+ * allows it (only those who manage the space's tabs can); a new key replaces this
61
+ * tab's last one. Fails with `DECLINED` if they don't, `FORBIDDEN` if they can't.
62
+ */
63
+ requestEventKey(): Promise<EventKey>;
64
+ /**
65
+ * Opens a new trigger in this space, starting on one of your manifest's events in this
66
+ * tab (or the first, without `event`): the person sets the task and saves it. Fails
67
+ * with `NO_TASKS_TAB` when the space has no Tasks tab to keep triggers in.
68
+ */
69
+ openTrigger(event?: string): Promise<void>;
70
+ /**
71
+ * Takes the person to a space, or one of its tabs (decision 177): a home page's
72
+ * links into the workspace. The workspace goes there; your page is left.
73
+ */
74
+ open(target: {
75
+ space: string;
76
+ tab?: string;
77
+ }): Promise<void>;
57
78
  /** Called when the reader switches between light and dark. Returns an unsubscribe. */
58
79
  onTheme(listener: (theme: Theme) => void): () => void;
59
80
  /** Stops listening. */
package/dist/client.js CHANGED
@@ -124,6 +124,19 @@ export function connect({ timeoutMs = 10_000, window: win = window } = {}) {
124
124
  async emit(name, fields = {}) {
125
125
  await ask((id) => ({ tands: 1, type: "event", id, name, data: fields }));
126
126
  },
127
+ requestEventKey: () => ask((id) => ({ tands: 1, type: "eventKey", id })),
128
+ async open(target) {
129
+ await ask((id) => ({
130
+ tands: 1,
131
+ type: "open",
132
+ id,
133
+ space: target.space,
134
+ tab: target.tab ?? null,
135
+ }));
136
+ },
137
+ async openTrigger(event) {
138
+ await ask((id) => ({ tands: 1, type: "openTrigger", id, event: event ?? null }));
139
+ },
127
140
  values(scope) {
128
141
  return {
129
142
  get: async (key) => (await data({ op: "values.get", scope, key })).value,
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Tabs & Spaces apps (M9). In a Custom tab's page: `connect()`. On your
2
+ * Tabs & Spaces apps (M9). In a Custom tab's or home page's page: `connect()`. On your
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 HostMessage, 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, } from "./protocol.js";
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Tabs & Spaces apps (M9). In a Custom tab's page: `connect()`. On your
2
+ * Tabs & Spaces apps (M9). In a Custom tab's or home page's page: `connect()`. On your
3
3
  * server: `verifyToken()` from "@tabs-and-spaces/sdk/server".
4
4
  */
5
5
  export { connect, WorkspaceError, } from "./client.js";
6
- export { PROTOCOL_VERSION, readAppMessage, readHostMessage, } from "./protocol.js";
6
+ export { PROTOCOL_VERSION, readAppMessage, readHostMessage, isHome, } from "./protocol.js";
@@ -5,7 +5,7 @@
5
5
  * tokens and theme changes. Every message carries `tands: 1`.
6
6
  */
7
7
  export declare const PROTOCOL_VERSION = 1;
8
- /** Who's looking, and where (also inside the token, signed). */
8
+ /** Who's looking, and where, in a Custom tab (also inside the token, signed). */
9
9
  export interface TabContext {
10
10
  user: {
11
11
  id: string;
@@ -35,7 +35,38 @@ export interface TabContext {
35
35
  };
36
36
  /** The reader's language, like "en" or "fr". */
37
37
  locale: string;
38
+ home?: undefined;
38
39
  }
40
+ /**
41
+ * Who's looking at one of your home pages (decision 177): the workspace's Home,
42
+ * so no space or tab around it. Guests see it too: `user.guest` says so.
43
+ */
44
+ export interface HomeContext {
45
+ user: {
46
+ id: string;
47
+ name: string;
48
+ guest: boolean;
49
+ };
50
+ workspace: {
51
+ id: string;
52
+ slug: string;
53
+ name: string;
54
+ };
55
+ space: null;
56
+ tab: null;
57
+ /** Which of your manifest's `home` pages this is. */
58
+ home: {
59
+ id: string;
60
+ };
61
+ app: {
62
+ id: string;
63
+ };
64
+ locale: string;
65
+ }
66
+ /** Where your page is: a Custom tab in a space, or the workspace's Home. */
67
+ export type PageContext = TabContext | HomeContext;
68
+ /** Whether your page is the workspace's Home (no space or tab around it). */
69
+ export declare const isHome: (context: PageContext) => context is HomeContext;
39
70
  export type Theme = "light" | "dark";
40
71
  /** Where app data lives, and so who sees it. */
41
72
  export type DataScope = "workspace" | "space" | "tab" | "user";
@@ -120,12 +151,44 @@ export type AppMessage = {
120
151
  id: string;
121
152
  name: string;
122
153
  data: Record<string, unknown>;
154
+ }
155
+ /**
156
+ * A key for the app's server to report events in this tab on its own (answered like
157
+ * data, with an `EventKey`), once the person allows it.
158
+ */
159
+ | {
160
+ tands: 1;
161
+ type: "eventKey";
162
+ id: string;
163
+ }
164
+ /**
165
+ * Opens a new trigger in this space, starting on one of the app's events in this tab
166
+ * (decision 174). Answered like data once it's open.
167
+ */
168
+ | {
169
+ tands: 1;
170
+ type: "openTrigger";
171
+ id: string;
172
+ event: string | null;
173
+ }
174
+ /** Takes the person to a space, or one of its tabs (decision 177). Answered like data. */
175
+ | {
176
+ tands: 1;
177
+ type: "open";
178
+ id: string;
179
+ space: string;
180
+ tab: string | null;
123
181
  };
182
+ /** What the app's server reports events with, at `eventsUrl` (decision 170). */
183
+ export interface EventKey {
184
+ key: string;
185
+ eventsUrl: string;
186
+ }
124
187
  /** From the workspace to the page. */
125
188
  export type HostMessage = {
126
189
  tands: 1;
127
190
  type: "context";
128
- context: TabContext;
191
+ context: PageContext;
129
192
  token: string;
130
193
  expiresAt: string;
131
194
  theme: Theme;
package/dist/protocol.js CHANGED
@@ -5,6 +5,8 @@
5
5
  * tokens and theme changes. Every message carries `tands: 1`.
6
6
  */
7
7
  export const PROTOCOL_VERSION = 1;
8
+ /** Whether your page is the workspace's Home (no space or tab around it). */
9
+ export const isHome = (context) => Boolean(context.home);
8
10
  const isRecord = (value) => typeof value === "object" && value !== null;
9
11
  /** A message from an app's page, or null. */
10
12
  export function readAppMessage(data) {
@@ -23,6 +25,35 @@ export function readAppMessage(data) {
23
25
  // Checked in full by the workspace's server.
24
26
  return { tands: 1, type: "data", id: data.id, request: data.request };
25
27
  }
28
+ if (data.type === "eventKey" && typeof data.id === "string" && data.id.length <= 64) {
29
+ return { tands: 1, type: "eventKey", id: data.id };
30
+ }
31
+ if (data.type === "openTrigger" &&
32
+ typeof data.id === "string" &&
33
+ data.id.length <= 64 &&
34
+ (data.event === undefined ||
35
+ data.event === null ||
36
+ (typeof data.event === "string" && data.event.length <= 40))) {
37
+ return {
38
+ tands: 1,
39
+ type: "openTrigger",
40
+ id: data.id,
41
+ event: typeof data.event === "string" ? data.event : null,
42
+ };
43
+ }
44
+ const id = (value) => typeof value === "string" && value.length > 0 && value.length <= 64;
45
+ if (data.type === "open" &&
46
+ id(data.id) &&
47
+ id(data.space) &&
48
+ (data.tab === undefined || data.tab === null || id(data.tab))) {
49
+ return {
50
+ tands: 1,
51
+ type: "open",
52
+ id: data.id,
53
+ space: data.space,
54
+ tab: typeof data.tab === "string" ? data.tab : null,
55
+ };
56
+ }
26
57
  if (data.type === "event" &&
27
58
  typeof data.id === "string" &&
28
59
  data.id.length <= 64 &&
package/dist/server.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- import type { DataRecord, DataRequest, DataScope, TabContext } from "./protocol.js";
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
3
  /**
3
4
  * Checking a Custom tab's token on your server (Node 22+, Deno, Bun, Workers:
4
5
  * anywhere with Web Crypto's Ed25519). The workspace signs it (EdDSA); its
@@ -17,7 +18,8 @@ export interface PublicKey {
17
18
  alg?: string;
18
19
  }
19
20
  export interface VerifiedToken {
20
- context: TabContext;
21
+ /** A Custom tab's, or a home page's (`isHome()`); an action's is always a tab's. */
22
+ context: PageContext;
21
23
  /** The deployment that signed it (its public address). */
22
24
  issuer: string;
23
25
  expiresAt: Date;
@@ -89,8 +91,19 @@ export declare function dataClient({ dataKey, dataUrl }: Pick<ActionRequest, "da
89
91
  delete: (id: string) => Promise<void>;
90
92
  };
91
93
  };
94
+ /**
95
+ * Reports an event from your server at any time (decision 170), with the event key
96
+ * your tab got from `requestEventKey()`: in that tab's space, for the person who
97
+ * allowed it. Keep the key secret, like a password.
98
+ *
99
+ * ```ts
100
+ * await reportEvent(stored, "pull_request_opened", { title: "Fix login", number: 42 });
101
+ * ```
102
+ */
103
+ export declare function reportEvent({ key, eventsUrl }: EventKey, name: string, fields?: Record<string, unknown>, fetcher?: typeof fetch): Promise<void>;
92
104
  /** A refusal from the workspace's data endpoint, with its code. */
93
105
  export declare class DataError extends Error {
94
106
  readonly code: string | null;
95
107
  constructor(message: string, code: string | null);
96
108
  }
109
+ export type { EventKey } from "./protocol.js";
package/dist/server.js CHANGED
@@ -1,3 +1,4 @@
1
+ export { isHome } from "./protocol.js";
1
2
  export class TokenError extends Error {
2
3
  }
3
4
  /** The deployment's public keys (cache them; they change only if its secret does). */
@@ -122,6 +123,26 @@ export function dataClient({ dataKey, dataUrl }, fetcher = fetch) {
122
123
  },
123
124
  };
124
125
  }
126
+ /**
127
+ * Reports an event from your server at any time (decision 170), with the event key
128
+ * your tab got from `requestEventKey()`: in that tab's space, for the person who
129
+ * allowed it. Keep the key secret, like a password.
130
+ *
131
+ * ```ts
132
+ * await reportEvent(stored, "pull_request_opened", { title: "Fix login", number: 42 });
133
+ * ```
134
+ */
135
+ export async function reportEvent({ key, eventsUrl }, name, fields = {}, fetcher = fetch) {
136
+ const response = await fetcher(eventsUrl, {
137
+ method: "POST",
138
+ headers: { "content-type": "application/json", authorization: `Bearer ${key}` },
139
+ body: JSON.stringify({ name, data: fields }),
140
+ });
141
+ if (!response.ok) {
142
+ const body = (await response.json().catch(() => null));
143
+ throw new DataError(body?.error?.message ?? `It answered ${response.status}.`, body?.error?.code ?? null);
144
+ }
145
+ }
125
146
  /** A refusal from the workspace's data endpoint, with its code. */
126
147
  export class DataError extends Error {
127
148
  code;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tabs-and-spaces/sdk",
3
- "version": "0.1.0",
3
+ "version": "0.3.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",