@dooc-dev/dooc 0.0.1 → 0.0.2
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/cli.js +69 -2
- package/dist/core/extension-package.d.ts +34 -0
- package/dist/core/extension-package.js +147 -0
- package/dist/core/icons.d.ts +1 -0
- package/dist/core/icons.js +1 -0
- package/dist/edit.global.js +86 -85
- package/dist/plugins/blocks.d.ts +6 -5
- package/dist/plugins/blocks.js +3 -2
- package/dist/plugins/index.d.ts +1 -1
- package/dist/plugins/local-storage.d.ts +18 -0
- package/dist/plugins/local-storage.js +115 -0
- package/dist/plugins/oauth.js +6 -0
- package/dist/plugins/runtime.d.ts +3 -1
- package/dist/plugins/runtime.js +69 -13
- package/dist/plugins/types.d.ts +3 -0
- package/dist/plugins/view.d.ts +1 -23
- package/dist/plugins/view.js +2 -41
- package/dist/ui.global.js +34 -33
- package/dist/viewer/extension-pack.d.ts +2 -0
- package/dist/viewer/extension-pack.js +36 -0
- package/dist/viewer/extensions.d.ts +10 -0
- package/dist/viewer/extensions.js +302 -0
- package/dist/viewer/hmr.js +2 -0
- package/dist/viewer/locales/eng.json +25 -2
- package/dist/viewer/locales/jpn.json +25 -2
- package/dist/viewer/locales/zho.json +25 -2
- package/dist/viewer/plugin-view.d.ts +0 -1
- package/dist/viewer/plugin-view.js +1 -23
- package/dist/viewer/plugins.d.ts +5 -1
- package/dist/viewer/plugins.js +70 -17
- package/dist/viewer/serve.d.ts +5 -1
- package/dist/viewer/serve.js +155 -76
- package/package.json +3 -2
package/dist/plugins/blocks.d.ts
CHANGED
|
@@ -75,18 +75,19 @@ export type PluginBlock = {
|
|
|
75
75
|
} | {
|
|
76
76
|
type: "divider";
|
|
77
77
|
};
|
|
78
|
+
export interface PluginIcon {
|
|
79
|
+
light: string;
|
|
80
|
+
dark?: string;
|
|
81
|
+
}
|
|
78
82
|
export interface PluginBlockContent {
|
|
79
|
-
format: "blocks";
|
|
80
83
|
header?: {
|
|
81
84
|
title: string;
|
|
82
85
|
context?: string;
|
|
83
86
|
reference?: string;
|
|
84
|
-
icon?:
|
|
85
|
-
light: string;
|
|
86
|
-
dark?: string;
|
|
87
|
-
};
|
|
87
|
+
icon?: PluginIcon;
|
|
88
88
|
};
|
|
89
89
|
blocks: PluginBlock[];
|
|
90
90
|
}
|
|
91
91
|
export declare const pluginLink: (value: unknown) => value is string;
|
|
92
|
+
export declare const validPluginIcon: (value: unknown) => value is PluginIcon;
|
|
92
93
|
export declare const validPluginBlocks: (value: unknown) => value is PluginBlockContent;
|
package/dist/plugins/blocks.js
CHANGED
|
@@ -17,6 +17,7 @@ export const pluginLink = (value) => {
|
|
|
17
17
|
const icon = (value) => typeof value === "string" &&
|
|
18
18
|
value.length <= 100_000 &&
|
|
19
19
|
/^data:image\/(?:svg\+xml|png|webp);base64,[A-Za-z0-9+/]+={0,2}$/.test(value);
|
|
20
|
+
export const validPluginIcon = (value) => record(value) && icon(value.light) && optional(value.dark, icon);
|
|
20
21
|
const edit = (value) => record(value) && text(value.action) && optional(value.field, text);
|
|
21
22
|
const inlines = (value) => array(value, (item) => text(item) ||
|
|
22
23
|
(record(item) &&
|
|
@@ -89,11 +90,11 @@ export const validPluginBlocks = (value) => {
|
|
|
89
90
|
}
|
|
90
91
|
});
|
|
91
92
|
return (record(value) &&
|
|
92
|
-
|
|
93
|
+
!("format" in value) &&
|
|
93
94
|
blocks(value.blocks, 0) &&
|
|
94
95
|
optional(value.header, (h) => record(h) &&
|
|
95
96
|
text(h.title) &&
|
|
96
97
|
optional(h.context, text) &&
|
|
97
98
|
optional(h.reference, text) &&
|
|
98
|
-
optional(h.icon,
|
|
99
|
+
optional(h.icon, validPluginIcon)));
|
|
99
100
|
};
|
package/dist/plugins/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export type { PluginAction, PluginActionContext, PluginActionField, PluginActionRequest, PluginActionResult, } from "./actions.js";
|
|
2
|
-
export type { PluginBlock, PluginBlockContent, PluginCell, PluginEdit, PluginInline, PluginTone, } from "./blocks.js";
|
|
2
|
+
export type { PluginBlock, PluginBlockContent, PluginCell, PluginEdit, PluginIcon, PluginInline, PluginTone, } from "./blocks.js";
|
|
3
3
|
export { PluginError, parseOAuthToken } from "./oauth.js";
|
|
4
4
|
export type { DataPlugin, OAuthToken, PluginContent, PluginContext, PluginOAuth, PluginUser, PluginView, } from "./types.js";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { DataPlugin, OAuthToken, PluginUser } from "./types.js";
|
|
2
|
+
export interface SavedConnection {
|
|
3
|
+
token: OAuthToken;
|
|
4
|
+
expires: number;
|
|
5
|
+
user: PluginUser;
|
|
6
|
+
}
|
|
7
|
+
export interface PluginCredentialStore {
|
|
8
|
+
get(plugin: DataPlugin): SavedConnection | undefined;
|
|
9
|
+
set(plugin: DataPlugin, value?: SavedConnection): void;
|
|
10
|
+
withLock?<T>(plugin: DataPlugin, run: () => Promise<T>): Promise<T>;
|
|
11
|
+
}
|
|
12
|
+
/** Personal state is outside document roots and never included in exported bundles. */
|
|
13
|
+
export declare const createLocalPluginStorage: (userDir?: string) => {
|
|
14
|
+
read: (name: string) => unknown;
|
|
15
|
+
write: (name: string, value: unknown) => void;
|
|
16
|
+
credentials: PluginCredentialStore;
|
|
17
|
+
};
|
|
18
|
+
export type LocalPluginStorage = ReturnType<typeof createLocalPluginStorage>;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import { lstatSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
6
|
+
import { parseOAuthToken } from "./oauth.js";
|
|
7
|
+
/** Personal state is outside document roots and never included in exported bundles. */
|
|
8
|
+
export const createLocalPluginStorage = (userDir = process.env.DOOC_USER_DIR ?? join(homedir(), ".dooc")) => {
|
|
9
|
+
const folder = join(userDir, "plugins");
|
|
10
|
+
const path = (name) => {
|
|
11
|
+
if (!/^[a-z]+-[a-f0-9]{64}\.json$/.test(name))
|
|
12
|
+
throw new Error("Invalid plugin storage key");
|
|
13
|
+
for (const directory of [userDir, folder]) {
|
|
14
|
+
const entry = lstatSync(directory, { throwIfNoEntry: false });
|
|
15
|
+
if (entry &&
|
|
16
|
+
(!entry.isDirectory() ||
|
|
17
|
+
entry.isSymbolicLink() ||
|
|
18
|
+
(process.platform !== "win32" &&
|
|
19
|
+
((entry.mode & 0o022) !== 0 || entry.uid !== process.getuid?.()))))
|
|
20
|
+
throw new Error("Personal plugin storage must be a local directory");
|
|
21
|
+
}
|
|
22
|
+
return join(folder, name);
|
|
23
|
+
};
|
|
24
|
+
const read = (name) => {
|
|
25
|
+
const file = path(name);
|
|
26
|
+
const entry = lstatSync(file, { throwIfNoEntry: false });
|
|
27
|
+
if (!entry)
|
|
28
|
+
return undefined;
|
|
29
|
+
if (!entry.isFile() ||
|
|
30
|
+
entry.isSymbolicLink() ||
|
|
31
|
+
entry.size > 1_048_576 ||
|
|
32
|
+
(process.platform !== "win32" &&
|
|
33
|
+
((entry.mode & 0o077) !== 0 || entry.uid !== process.getuid?.())))
|
|
34
|
+
throw new Error("Personal plugin files must be private to the current user");
|
|
35
|
+
return JSON.parse(readFileSync(file, "utf8"));
|
|
36
|
+
};
|
|
37
|
+
const write = (name, value) => {
|
|
38
|
+
const file = path(name);
|
|
39
|
+
mkdirSync(folder, { recursive: true, mode: 0o700 });
|
|
40
|
+
const temporary = `${file}.${randomBytes(8).toString("hex")}.tmp`;
|
|
41
|
+
try {
|
|
42
|
+
writeFileSync(temporary, JSON.stringify(value), { flag: "wx", mode: 0o600 });
|
|
43
|
+
renameSync(temporary, file);
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
rmSync(temporary, { force: true });
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
const key = (plugin) => `credentials-${createHash("sha256")
|
|
50
|
+
.update(JSON.stringify([
|
|
51
|
+
plugin.id,
|
|
52
|
+
plugin.oauth.authorizationUrl,
|
|
53
|
+
plugin.oauth.tokenUrl,
|
|
54
|
+
plugin.oauth.clientId,
|
|
55
|
+
plugin.oauth.clientSecret ?? "",
|
|
56
|
+
[...plugin.oauth.scopes].sort(),
|
|
57
|
+
[...plugin.apiOrigins].sort(),
|
|
58
|
+
plugin.oauth.authorizationParams ?? {},
|
|
59
|
+
]))
|
|
60
|
+
.digest("hex")}.json`;
|
|
61
|
+
const credentials = {
|
|
62
|
+
get(plugin) {
|
|
63
|
+
const value = read(key(plugin));
|
|
64
|
+
if (!value)
|
|
65
|
+
return undefined;
|
|
66
|
+
const token = parseOAuthToken(value.token);
|
|
67
|
+
if (!value.user ||
|
|
68
|
+
typeof value.user.id !== "string" ||
|
|
69
|
+
!value.user.id ||
|
|
70
|
+
value.user.id.length > 512 ||
|
|
71
|
+
typeof value.user.name !== "string" ||
|
|
72
|
+
!value.user.name ||
|
|
73
|
+
value.user.name.length > 512 ||
|
|
74
|
+
(value.expires !== null &&
|
|
75
|
+
(typeof value.expires !== "number" || !Number.isFinite(value.expires))))
|
|
76
|
+
throw new Error("Invalid saved plugin login");
|
|
77
|
+
return { token, user: value.user, expires: value.expires ?? Infinity };
|
|
78
|
+
},
|
|
79
|
+
set(plugin, value) {
|
|
80
|
+
write(key(plugin), value ?? null);
|
|
81
|
+
},
|
|
82
|
+
async withLock(plugin, run) {
|
|
83
|
+
const lock = `${path(key(plugin))}.lock`;
|
|
84
|
+
mkdirSync(folder, { recursive: true, mode: 0o700 });
|
|
85
|
+
const owner = randomBytes(16).toString("hex");
|
|
86
|
+
const deadline = Date.now() + 20000;
|
|
87
|
+
while (true) {
|
|
88
|
+
try {
|
|
89
|
+
writeFileSync(lock, owner, { flag: "wx", mode: 0o600 });
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
if (error.code !== "EEXIST")
|
|
94
|
+
throw error;
|
|
95
|
+
const entry = lstatSync(lock, { throwIfNoEntry: false });
|
|
96
|
+
if (entry?.isSymbolicLink())
|
|
97
|
+
throw new Error("Invalid login lock");
|
|
98
|
+
if (entry && Date.now() - entry.mtimeMs > 30000)
|
|
99
|
+
rmSync(lock, { force: true });
|
|
100
|
+
if (Date.now() >= deadline)
|
|
101
|
+
throw new Error("Another dooc process is refreshing this login. Try again.");
|
|
102
|
+
await delay(50);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
return await run();
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
if (readFileSync(lock, "utf8") === owner)
|
|
110
|
+
rmSync(lock, { force: true });
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
return { read, write, credentials };
|
|
115
|
+
};
|
package/dist/plugins/oauth.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { validPluginIcon } from "./blocks.js";
|
|
1
2
|
export class PluginError extends Error {
|
|
2
3
|
status;
|
|
3
4
|
name = "DoocPluginError";
|
|
@@ -112,6 +113,11 @@ export const validatePlugins = (input) => {
|
|
|
112
113
|
ids.add(plugin.id);
|
|
113
114
|
const oauth = plugin.oauth;
|
|
114
115
|
if (!oauth ||
|
|
116
|
+
(plugin.name !== undefined &&
|
|
117
|
+
(typeof plugin.name !== "string" ||
|
|
118
|
+
!plugin.name.trim() ||
|
|
119
|
+
plugin.name.length > 100)) ||
|
|
120
|
+
(plugin.icon !== undefined && !validPluginIcon(plugin.icon)) ||
|
|
115
121
|
typeof oauth.clientId !== "string" ||
|
|
116
122
|
!oauth.clientId.trim() ||
|
|
117
123
|
(oauth.clientSecret !== undefined && typeof oauth.clientSecret !== "string") ||
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type PluginActionResult } from "./actions.js";
|
|
2
|
+
import type { PluginCredentialStore } from "./local-storage.js";
|
|
2
3
|
import type { DataPlugin, PluginUser } from "./types.js";
|
|
3
|
-
export declare const createPluginRuntime: (definitions: readonly DataPlugin[], transport?: typeof fetch) => {
|
|
4
|
+
export declare const createPluginRuntime: (definitions: readonly DataPlugin[], transport?: typeof fetch, credentials?: PluginCredentialStore) => {
|
|
4
5
|
resolve(source: unknown): string | undefined;
|
|
5
6
|
register(plugin: DataPlugin): void;
|
|
6
7
|
unregister: (pluginId: string) => void;
|
|
@@ -9,6 +10,7 @@ export declare const createPluginRuntime: (definitions: readonly DataPlugin[], t
|
|
|
9
10
|
connect(id: string, pluginId: string, redirectUri: string): string;
|
|
10
11
|
callback(id: string, pluginId: string, redirectUri: string, params: URLSearchParams): Promise<void>;
|
|
11
12
|
disconnect(id: string, pluginId: string): void;
|
|
13
|
+
status(id: string, pluginId: string): PluginUser | undefined;
|
|
12
14
|
read(id: string, pluginId: string, source: unknown): Promise<{
|
|
13
15
|
user: PluginUser;
|
|
14
16
|
view: import("./view.js").PluginView;
|
package/dist/plugins/runtime.js
CHANGED
|
@@ -5,9 +5,9 @@ import { parsePluginUrl, validatePluginView } from "./view.js";
|
|
|
5
5
|
const nonce = () => randomBytes(32).toString("base64url");
|
|
6
6
|
const expired = () => new PluginError(401, "Connect this plugin again");
|
|
7
7
|
const expiry = (token) => token.expires_in === undefined ? Infinity : Date.now() + token.expires_in * 1000;
|
|
8
|
-
export const createPluginRuntime = (definitions, transport) => {
|
|
8
|
+
export const createPluginRuntime = (definitions, transport, credentials) => {
|
|
9
9
|
const plugins = new Map(validatePlugins(definitions).map((plugin) => [plugin.id, plugin]));
|
|
10
|
-
//
|
|
10
|
+
// Browser sessions expire independently of optional personal credential persistence.
|
|
11
11
|
const sessions = new Map();
|
|
12
12
|
const sessionFor = (id) => {
|
|
13
13
|
const session = sessions.get(id);
|
|
@@ -96,27 +96,62 @@ export const createPluginRuntime = (definitions, transport) => {
|
|
|
96
96
|
const plugin = pluginFor(pluginId);
|
|
97
97
|
if (!plugin.matches(url))
|
|
98
98
|
throw new PluginError(400, "URL is not supported by this plugin");
|
|
99
|
-
|
|
100
|
-
if (
|
|
99
|
+
let restored = session.connections.get(pluginId);
|
|
100
|
+
if (credentials) {
|
|
101
|
+
const saved = credentials.get(plugin);
|
|
102
|
+
if (!saved) {
|
|
103
|
+
session.connections.delete(pluginId);
|
|
104
|
+
throw expired();
|
|
105
|
+
}
|
|
106
|
+
if (!restored || restored.user.id !== saved.user.id) {
|
|
107
|
+
restored = saved;
|
|
108
|
+
session.connections.set(pluginId, restored);
|
|
109
|
+
}
|
|
110
|
+
else if (restored.token.access_token !== saved.token.access_token) {
|
|
111
|
+
restored.token = saved.token;
|
|
112
|
+
restored.expires = saved.expires;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (!restored)
|
|
101
116
|
throw expired();
|
|
117
|
+
const connection = restored;
|
|
102
118
|
try {
|
|
103
119
|
if (connection.expires <= Date.now() + 30_000) {
|
|
104
120
|
const refreshToken = connection.token.refresh_token;
|
|
105
121
|
if (!refreshToken)
|
|
106
122
|
throw expired();
|
|
107
123
|
connection.refreshing ??= (async () => {
|
|
108
|
-
|
|
124
|
+
const refresh = async () => {
|
|
125
|
+
const saved = credentials?.get(plugin);
|
|
126
|
+
if (credentials && (!saved || saved.user.id !== connection.user.id))
|
|
127
|
+
throw expired();
|
|
128
|
+
if (saved && saved.token.access_token !== connection.token.access_token) {
|
|
129
|
+
connection.token = saved.token;
|
|
130
|
+
connection.expires = saved.expires;
|
|
131
|
+
if (connection.expires > Date.now() + 30_000)
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const previous = connection.token.access_token;
|
|
135
|
+
const usedRefresh = connection.token.refresh_token ?? refreshToken;
|
|
109
136
|
const token = await exchange(plugin, {
|
|
110
137
|
grant_type: "refresh_token",
|
|
111
|
-
refresh_token:
|
|
138
|
+
refresh_token: usedRefresh,
|
|
112
139
|
});
|
|
113
140
|
assertCurrent(id, pluginId, connection);
|
|
141
|
+
if (credentials && credentials.get(plugin)?.token.access_token !== previous)
|
|
142
|
+
throw expired();
|
|
114
143
|
connection.token = {
|
|
115
144
|
...token,
|
|
116
|
-
refresh_token: token.refresh_token ??
|
|
145
|
+
refresh_token: token.refresh_token ?? usedRefresh,
|
|
117
146
|
scope: token.scope ?? connection.token.scope,
|
|
118
147
|
};
|
|
119
148
|
connection.expires = expiry(token);
|
|
149
|
+
credentials?.set(plugin, connection);
|
|
150
|
+
};
|
|
151
|
+
try {
|
|
152
|
+
await (credentials?.withLock
|
|
153
|
+
? credentials.withLock(plugin, refresh)
|
|
154
|
+
: refresh());
|
|
120
155
|
}
|
|
121
156
|
finally {
|
|
122
157
|
connection.refreshing = undefined;
|
|
@@ -132,8 +167,12 @@ export const createPluginRuntime = (definitions, transport) => {
|
|
|
132
167
|
catch (error) {
|
|
133
168
|
if (isPluginError(error) &&
|
|
134
169
|
error.status === 401 &&
|
|
135
|
-
session.connections.get(pluginId) === connection)
|
|
170
|
+
session.connections.get(pluginId) === connection) {
|
|
136
171
|
session.connections.delete(pluginId);
|
|
172
|
+
const saved = credentials?.get(plugin);
|
|
173
|
+
if (saved?.token.access_token === connection.token.access_token)
|
|
174
|
+
credentials?.set(plugin);
|
|
175
|
+
}
|
|
137
176
|
throw error;
|
|
138
177
|
}
|
|
139
178
|
};
|
|
@@ -196,6 +235,10 @@ export const createPluginRuntime = (definitions, transport) => {
|
|
|
196
235
|
expires: Date.now() + 10 * 60 * 1000,
|
|
197
236
|
};
|
|
198
237
|
session.connections.delete(pluginId);
|
|
238
|
+
credentials?.set(plugin);
|
|
239
|
+
if (credentials)
|
|
240
|
+
for (const other of sessions.values())
|
|
241
|
+
other.connections.delete(pluginId);
|
|
199
242
|
session.attempts.set(pluginId, attempt);
|
|
200
243
|
const url = new URL(plugin.oauth.authorizationUrl);
|
|
201
244
|
for (const [key, value] of Object.entries(plugin.oauth.authorizationParams ?? {}))
|
|
@@ -246,16 +289,29 @@ export const createPluginRuntime = (definitions, transport) => {
|
|
|
246
289
|
if (sessionFor(id).attempts.get(pluginId) !== attempt)
|
|
247
290
|
throw expired();
|
|
248
291
|
session.attempts.delete(pluginId);
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
user: { id: user.id, name: user.name },
|
|
253
|
-
});
|
|
292
|
+
const connection = { token, expires, user: { id: user.id, name: user.name } };
|
|
293
|
+
credentials?.set(plugin, connection);
|
|
294
|
+
session.connections.set(pluginId, connection);
|
|
254
295
|
},
|
|
255
296
|
disconnect(id, pluginId) {
|
|
256
297
|
const session = sessionFor(id);
|
|
257
298
|
session.connections.delete(pluginId);
|
|
258
299
|
session.attempts.delete(pluginId);
|
|
300
|
+
credentials?.set(pluginFor(pluginId));
|
|
301
|
+
if (credentials)
|
|
302
|
+
for (const other of sessions.values())
|
|
303
|
+
other.connections.delete(pluginId);
|
|
304
|
+
},
|
|
305
|
+
status(id, pluginId) {
|
|
306
|
+
const plugin = pluginFor(pluginId);
|
|
307
|
+
const session = sessionFor(id);
|
|
308
|
+
const connection = credentials
|
|
309
|
+
? credentials.get(plugin)
|
|
310
|
+
: session.connections.get(pluginId);
|
|
311
|
+
if (!connection ||
|
|
312
|
+
(connection.expires <= Date.now() && !connection.token.refresh_token))
|
|
313
|
+
return undefined;
|
|
314
|
+
return { ...connection.user };
|
|
259
315
|
},
|
|
260
316
|
read(id, pluginId, source) {
|
|
261
317
|
return access(id, pluginId, source, async (plugin, url, connection) => ({
|
package/dist/plugins/types.d.ts
CHANGED
|
@@ -32,10 +32,13 @@ export interface PluginContext {
|
|
|
32
32
|
}
|
|
33
33
|
export type { PluginContent, PluginView } from "./view.js";
|
|
34
34
|
import type { PluginAction, PluginActionContext, PluginActionRequest, PluginActionResult } from "./actions.js";
|
|
35
|
+
import type { PluginIcon } from "./blocks.js";
|
|
35
36
|
import type { PluginView } from "./view.js";
|
|
36
37
|
/** Trusted server code injected into the viewer, independently of document configuration. */
|
|
37
38
|
export interface DataPlugin {
|
|
38
39
|
id: string;
|
|
40
|
+
name?: string;
|
|
41
|
+
icon?: PluginIcon;
|
|
39
42
|
/** Match a service URL without fetching it; unmatched embeds remain ordinary links. */
|
|
40
43
|
matches: (url: URL) => boolean;
|
|
41
44
|
oauth: PluginOAuth;
|
package/dist/plugins/view.d.ts
CHANGED
|
@@ -1,27 +1,5 @@
|
|
|
1
1
|
import { type PluginBlockContent } from "./blocks.js";
|
|
2
|
-
export type PluginContent = PluginBlockContent
|
|
3
|
-
format: "html";
|
|
4
|
-
html: string;
|
|
5
|
-
height?: number | "auto";
|
|
6
|
-
} | {
|
|
7
|
-
format: "json";
|
|
8
|
-
preset: "card";
|
|
9
|
-
title: string;
|
|
10
|
-
text: string;
|
|
11
|
-
} | {
|
|
12
|
-
format: "json";
|
|
13
|
-
preset: "list";
|
|
14
|
-
items: string[];
|
|
15
|
-
} | {
|
|
16
|
-
format: "json";
|
|
17
|
-
preset: "table";
|
|
18
|
-
headers: string[];
|
|
19
|
-
rows: (string | number | boolean | null)[][];
|
|
20
|
-
} | {
|
|
21
|
-
format: "json";
|
|
22
|
-
preset: "code";
|
|
23
|
-
value: unknown;
|
|
24
|
-
};
|
|
2
|
+
export type PluginContent = PluginBlockContent;
|
|
25
3
|
export type PluginView = PluginContent & {
|
|
26
4
|
collapsed?: PluginContent;
|
|
27
5
|
};
|
package/dist/plugins/view.js
CHANGED
|
@@ -1,44 +1,4 @@
|
|
|
1
1
|
import { validPluginBlocks } from "./blocks.js";
|
|
2
|
-
const validContent = (view) => {
|
|
3
|
-
let valid = false;
|
|
4
|
-
if (view && typeof view === "object") {
|
|
5
|
-
if (view.format === "blocks")
|
|
6
|
-
return validPluginBlocks(view);
|
|
7
|
-
if (view.format === "html")
|
|
8
|
-
valid =
|
|
9
|
-
typeof view.html === "string" &&
|
|
10
|
-
(view.height === undefined ||
|
|
11
|
-
view.height === "auto" ||
|
|
12
|
-
(Number.isInteger(view.height) && view.height >= 32 && view.height <= 1200));
|
|
13
|
-
if (view.format === "json") {
|
|
14
|
-
switch (view.preset) {
|
|
15
|
-
case "card":
|
|
16
|
-
valid = typeof view.title === "string" && typeof view.text === "string";
|
|
17
|
-
break;
|
|
18
|
-
case "list":
|
|
19
|
-
valid =
|
|
20
|
-
Array.isArray(view.items) &&
|
|
21
|
-
view.items.every((item) => typeof item === "string");
|
|
22
|
-
break;
|
|
23
|
-
case "table":
|
|
24
|
-
valid =
|
|
25
|
-
Array.isArray(view.headers) &&
|
|
26
|
-
view.headers.every((h) => typeof h === "string") &&
|
|
27
|
-
Array.isArray(view.rows) &&
|
|
28
|
-
view.rows.every((row) => Array.isArray(row) &&
|
|
29
|
-
row.length === view.headers.length &&
|
|
30
|
-
row.every((cell) => cell === null ||
|
|
31
|
-
["string", "boolean"].includes(typeof cell) ||
|
|
32
|
-
(typeof cell === "number" && Number.isFinite(cell))));
|
|
33
|
-
break;
|
|
34
|
-
case "code":
|
|
35
|
-
valid = view.value !== undefined;
|
|
36
|
-
break;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
return valid;
|
|
41
|
-
};
|
|
42
2
|
export const validatePluginView = (input) => {
|
|
43
3
|
const encoded = JSON.stringify(input);
|
|
44
4
|
if (typeof encoded !== "string")
|
|
@@ -46,7 +6,8 @@ export const validatePluginView = (input) => {
|
|
|
46
6
|
if (new TextEncoder().encode(encoded).length > 2 * 1024 * 1024)
|
|
47
7
|
throw new Error("Plugin view is too large");
|
|
48
8
|
const view = JSON.parse(encoded);
|
|
49
|
-
if (!
|
|
9
|
+
if (!validPluginBlocks(view) ||
|
|
10
|
+
(view.collapsed !== undefined && !validPluginBlocks(view.collapsed)))
|
|
50
11
|
throw new Error("Invalid plugin view");
|
|
51
12
|
return view;
|
|
52
13
|
};
|