@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 +23 -2
- package/dist/client.js +13 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/protocol.d.ts +65 -2
- package/dist/protocol.js +31 -0
- package/dist/server.d.ts +15 -2
- package/dist/server.js +21 -0
- package/package.json +1 -1
package/dist/client.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type DataRecord, type DataRequest, type DataScope, type
|
|
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:
|
|
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";
|
package/dist/protocol.d.ts
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
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.
|
|
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",
|