@capybara-hq/kernel 0.1.0-rc.13 → 0.1.0-rc.15
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/admin/admin.schemas.d.ts +5 -13
- package/dist/catalog/admin-catalog.controller.d.ts +39 -5
- package/dist/catalog/admin-catalog.controller.js +75 -6
- package/dist/catalog/admin-catalog.schemas.d.ts +10 -0
- package/dist/catalog/admin-catalog.schemas.js +7 -0
- package/dist/catalog/appearance.schemas.d.ts +10 -0
- package/dist/catalog/appearance.schemas.js +20 -0
- package/dist/catalog/appearance.service.d.ts +105 -0
- package/dist/catalog/appearance.service.js +306 -0
- package/dist/catalog/catalog-content.d.ts +9 -1
- package/dist/catalog/catalog-content.js +2 -1
- package/dist/catalog/catalog-files.d.ts +3 -2
- package/dist/catalog/catalog-files.js +8 -5
- package/dist/catalog/catalog-import.module.js +3 -0
- package/dist/catalog/catalog-items.d.ts +11 -0
- package/dist/catalog/catalog-items.js +21 -0
- package/dist/catalog/catalog-scope.service.d.ts +7 -0
- package/dist/catalog/catalog-scope.service.js +55 -15
- package/dist/catalog/catalog.service.js +10 -2
- package/dist/catalog/cli.js +3 -0
- package/dist/catalog/component-scope.service.d.ts +14 -1
- package/dist/catalog/component-scope.service.js +12 -2
- package/dist/catalog/marketplaces.service.d.ts +1 -1
- package/dist/catalog/org-catalog.controller.d.ts +5 -4
- package/dist/catalog/org-catalog.controller.js +5 -4
- package/dist/catalog/org-catalog.schemas.d.ts +18 -2
- package/dist/catalog/org-catalog.schemas.js +16 -2
- package/dist/catalog/org-catalog.service.js +8 -2
- package/dist/catalog/org-extension.controller.d.ts +27 -7
- package/dist/catalog/org-extension.controller.js +48 -6
- package/dist/catalog/plugin-import.d.ts +28 -4
- package/dist/catalog/plugin-import.js +96 -20
- package/dist/catalog/plugin-package.d.ts +4 -2
- package/dist/catalog/plugin-package.js +66 -12
- package/dist/catalog/plugin-upgrade.d.ts +15 -1
- package/dist/catalog/plugin-upgrade.js +18 -5
- package/dist/channels/connections.controller.d.ts +1 -1
- package/dist/channels/connections.controller.js +1 -1
- package/dist/channels/connections.service.d.ts +22 -5
- package/dist/channels/connections.service.js +56 -10
- package/dist/channels/webhook.d.ts +21 -1
- package/dist/channels/webhook.js +39 -4
- package/dist/client/client-access.service.d.ts +101 -0
- package/dist/client/client-access.service.js +294 -0
- package/dist/client/client-builds.service.d.ts +109 -0
- package/dist/client/client-builds.service.js +229 -0
- package/dist/client/client-delivery.controller.d.ts +25 -0
- package/dist/client/client-delivery.controller.js +66 -0
- package/dist/client/client-files.d.ts +48 -0
- package/dist/client/client-files.js +136 -0
- package/dist/client/client-reports.cron.d.ts +13 -0
- package/dist/client/client-reports.cron.js +42 -0
- package/dist/client/client-reports.service.d.ts +30 -0
- package/dist/client/client-reports.service.js +102 -0
- package/dist/client/client.controller.d.ts +45 -0
- package/dist/client/client.controller.js +121 -0
- package/dist/client/client.module.d.ts +7 -0
- package/dist/client/client.module.js +26 -0
- package/dist/client/client.schemas.d.ts +55 -0
- package/dist/client/client.schemas.js +27 -0
- package/dist/ctx/create-ctx.d.ts +19 -4
- package/dist/ctx/create-ctx.js +51 -3
- package/dist/db/kernel-schema.d.ts +58 -2
- package/dist/errors.d.ts +33 -0
- package/dist/errors.js +33 -0
- package/dist/extensions/admin-ops.d.ts +4 -1
- package/dist/extensions/admin-ops.js +22 -1
- package/dist/extensions/admin-view.d.ts +8 -0
- package/dist/extensions/admin-view.js +78 -0
- package/dist/extensions/blobs.d.ts +14 -1
- package/dist/extensions/blobs.js +48 -5
- package/dist/extensions/guide-places.d.ts +66 -0
- package/dist/extensions/guide-places.js +152 -0
- package/dist/extensions/host.d.ts +6 -1
- package/dist/extensions/host.js +23 -12
- package/dist/extensions/instance.d.ts +5 -1
- package/dist/extensions/instance.js +8 -0
- package/dist/extensions/ledger.d.ts +1 -1
- package/dist/extensions/package.d.ts +4 -3
- package/dist/extensions/package.js +5 -1
- package/dist/extensions/permission-set.d.ts +35 -5
- package/dist/extensions/permission-set.js +36 -5
- package/dist/extensions/permission-view.d.ts +38 -7
- package/dist/extensions/permission-view.js +27 -8
- package/dist/extensions/plugin-guides.d.ts +207 -54
- package/dist/extensions/plugin-guides.js +436 -165
- package/dist/extensions/sources.d.ts +6 -0
- package/dist/extensions/sources.js +2 -0
- package/dist/extensions/state.module.js +9 -1
- package/dist/http/api.module.js +16 -0
- package/dist/identity/settings.d.ts +41 -26
- package/dist/identity/settings.js +87 -4
- package/dist/infra/lock/lock.service.d.ts +8 -0
- package/dist/infra/lock/lock.service.js +27 -0
- package/dist/infra/throttler/named-throttlers.d.ts +8 -1
- package/dist/infra/throttler/named-throttlers.js +30 -1
- package/dist/infra/throttler/throttler.guard.js +1 -1
- package/dist/kernel.module.js +3 -0
- package/dist/orgs/settings.service.d.ts +2 -26
- package/dist/overrides.d.ts +3 -1
- package/dist/overrides.js +2 -0
- package/dist/platform/exclusions.js +37 -6
- package/dist/platform/operation.js +4 -0
- package/dist/platform/ops/admin.ops.js +17 -2
- package/dist/platform/ops/catalog-admin.ops.d.ts +19 -1
- package/dist/platform/ops/catalog-admin.ops.js +85 -2
- package/dist/platform/ops/connectors.ops.js +2 -0
- package/dist/platform/ops/org-plugins.ops.d.ts +7 -1
- package/dist/platform/ops/org-plugins.ops.js +63 -4
- package/dist/platform/ops/org-settings.ops.js +2 -2
- package/dist/platform/ops/orgs.ops.js +1 -0
- package/dist/platform/ops/setup.ops.d.ts +24 -0
- package/dist/platform/ops/setup.ops.js +212 -0
- package/dist/platform/ops/ui.ops.js +1 -1
- package/dist/platform/platform-ops.module.d.ts +2 -1
- package/dist/platform/platform-ops.module.js +7 -0
- package/dist/platform/route-schemas.js +8 -0
- package/dist/roles/worker.module.js +4 -0
- package/dist/setup/kernel-checks.d.ts +91 -0
- package/dist/setup/kernel-checks.js +295 -0
- package/dist/setup/oauth-token-probe.d.ts +47 -0
- package/dist/setup/oauth-token-probe.js +108 -0
- package/dist/setup/redact-check.d.ts +18 -0
- package/dist/setup/redact-check.js +46 -0
- package/dist/setup/setup-check-registry.d.ts +86 -0
- package/dist/setup/setup-check-registry.js +75 -0
- package/dist/setup/setup-checks.controller.d.ts +28 -0
- package/dist/setup/setup-checks.controller.js +78 -0
- package/dist/setup/setup-checks.schemas.d.ts +38 -0
- package/dist/setup/setup-checks.schemas.js +43 -0
- package/dist/setup/setup-checks.service.d.ts +144 -0
- package/dist/setup/setup-checks.service.js +683 -0
- package/dist/setup/setup-guides.controller.d.ts +18 -0
- package/dist/setup/setup-guides.controller.js +48 -0
- package/dist/setup/setup-guides.schemas.d.ts +10 -0
- package/dist/setup/setup-guides.schemas.js +6 -0
- package/dist/setup/setup-guides.service.d.ts +126 -0
- package/dist/setup/setup-guides.service.js +315 -0
- package/dist/setup/setup.module.d.ts +8 -0
- package/dist/setup/setup.module.js +31 -0
- package/dist/themes/render-css.d.ts +2 -1
- package/dist/themes/render-css.js +5 -2
- package/dist/themes/theme-delivery.service.d.ts +4 -2
- package/dist/themes/theme-delivery.service.js +8 -3
- package/dist/themes/theme-state.d.ts +28 -7
- package/dist/themes/theme-state.js +24 -6
- package/dist/themes/themes-public.controller.d.ts +7 -2
- package/dist/themes/themes-public.controller.js +11 -5
- package/dist/themes/themes.service.js +2 -2
- package/dist/tools/connectors.controller.d.ts +28 -11
- package/dist/tools/connectors.controller.js +49 -14
- package/dist/tools/connectors.schemas.d.ts +4 -0
- package/dist/tools/connectors.schemas.js +4 -0
- package/dist/tools/connectors.service.d.ts +30 -2
- package/dist/tools/connectors.service.js +98 -3
- package/dist/tools/credentials.d.ts +20 -0
- package/dist/tools/credentials.js +90 -1
- package/dist/tools/oauth/tokens.d.ts +20 -0
- package/dist/tools/oauth/tokens.js +49 -24
- package/dist/tools/pipeline.d.ts +3 -2
- package/dist/ui/arrangement.d.ts +41 -50
- package/dist/ui/arrangement.js +88 -15
- package/dist/ui/registry.d.ts +13 -3
- package/dist/ui/registry.js +15 -4
- package/dist/ui/ui.schemas.d.ts +4 -0
- package/dist/ui/views.d.ts +36 -9
- package/dist/ui/views.js +58 -28
- package/package.json +3 -3
- package/src/migrations/0106_setup_guides_v2.sql +10 -0
- package/src/migrations/0107_extension_client_builds.sql +50 -0
|
@@ -119,6 +119,26 @@ export declare class Credentials {
|
|
|
119
119
|
* 都会更新写入时间),最多 `limit` 个。漂移检查用它挑个人凭据型连接器的凭据。
|
|
120
120
|
*/
|
|
121
121
|
personalHolders(orgId: string, connectorId: string, limit: number): Promise<string[]>;
|
|
122
|
+
/**
|
|
123
|
+
* For setup checks (setup guides v2 §10.4): the input revision of a connector's credentials,
|
|
124
|
+
* the `updated_at` of its OAuth client row, its org credential row and, given `accountId`, that
|
|
125
|
+
* member's row (ISO), never a value. Reads no secret.
|
|
126
|
+
*/
|
|
127
|
+
checkRevision(orgId: string, connectorId: string, accountId: string | null): Promise<string>;
|
|
128
|
+
/**
|
|
129
|
+
* For setup checks (setup guides v2 §10.8), never refreshing or writing anything: every stored
|
|
130
|
+
* credential value of a connector (decrypted: the OAuth client secret and DCR registration
|
|
131
|
+
* token, the org credential, every member's access and refresh tokens or static values) that a
|
|
132
|
+
* check result is redacted by, with the forms they are sent in (what `injectionSecrets` covers
|
|
133
|
+
* for a tool call: the Basic header value, its base64 token and decoded `user:pass`, the
|
|
134
|
+
* `encodeURIComponent` forms; for the client also the `client_secret_basic` header the token
|
|
135
|
+
* request sends and the raw `id:secret`), and the stored OAuth client id (null without a
|
|
136
|
+
* client). Never echo the values; a plugin check gets only a manual client's id.
|
|
137
|
+
*/
|
|
138
|
+
checkMaterial(orgId: string, connectorId: string): Promise<{
|
|
139
|
+
secrets: string[];
|
|
140
|
+
clientId: string | null;
|
|
141
|
+
}>;
|
|
122
142
|
/** 只看与 auth.ownership 对应的那一条凭据是否存在(不解密);none 恒为 connected。 */
|
|
123
143
|
status(orgId: string, connectorId: string, auth: ConnectorAuth, accountId: string): Promise<"connected" | "missing">;
|
|
124
144
|
/** 返回本次请求要注入的头 / 查询参数;缺凭据抛 CredentialMissing。 */
|
|
@@ -17,7 +17,7 @@ import { KernelDb } from "../infra/database/kernel-db.js";
|
|
|
17
17
|
import { LockService } from "../infra/lock/lock.service.js";
|
|
18
18
|
import { KERNEL_LOGGER, KEYRING, REDIS } from "../infra/tokens.js";
|
|
19
19
|
import { oauthHttpOptions } from "./oauth/discovery.js";
|
|
20
|
-
import { OAuthReconnectRequired, OAuthTokens, } from "./oauth/tokens.js";
|
|
20
|
+
import { basicClientAuth, OAuthReconnectRequired, OAuthTokens, } from "./oauth/tokens.js";
|
|
21
21
|
import { injectionSecrets } from "./redact.js";
|
|
22
22
|
export const isOAuth = (auth) => auth.type === "oauth_code" || auth.type === "oauth_client";
|
|
23
23
|
/** 缺凭据:只带连接器 id 与归属,绝不带值。 */
|
|
@@ -239,6 +239,95 @@ let Credentials = class Credentials {
|
|
|
239
239
|
LIMIT ${limit}`.execute(this.db.db);
|
|
240
240
|
return rows.rows.map((r) => r.account_id);
|
|
241
241
|
}
|
|
242
|
+
/**
|
|
243
|
+
* For setup checks (setup guides v2 §10.4): the input revision of a connector's credentials,
|
|
244
|
+
* the `updated_at` of its OAuth client row, its org credential row and, given `accountId`, that
|
|
245
|
+
* member's row (ISO), never a value. Reads no secret.
|
|
246
|
+
*/
|
|
247
|
+
async checkRevision(orgId, connectorId, accountId) {
|
|
248
|
+
const keys = [clientKey(connectorId), orgKey(connectorId)];
|
|
249
|
+
if (accountId !== null)
|
|
250
|
+
keys.push(userKey(connectorId, accountId));
|
|
251
|
+
const rows = await this.db
|
|
252
|
+
.selectFrom("secrets")
|
|
253
|
+
.select(["key", "updated_at"])
|
|
254
|
+
.where("namespace", "=", NAMESPACE)
|
|
255
|
+
.where("org_id", "=", orgId)
|
|
256
|
+
.where("key", "in", keys)
|
|
257
|
+
.execute();
|
|
258
|
+
const at = new Map(rows.map((r) => [r.key, new Date(r.updated_at).toISOString()]));
|
|
259
|
+
return keys.map((k) => at.get(k) ?? "-").join("|");
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* For setup checks (setup guides v2 §10.8), never refreshing or writing anything: every stored
|
|
263
|
+
* credential value of a connector (decrypted: the OAuth client secret and DCR registration
|
|
264
|
+
* token, the org credential, every member's access and refresh tokens or static values) that a
|
|
265
|
+
* check result is redacted by, with the forms they are sent in (what `injectionSecrets` covers
|
|
266
|
+
* for a tool call: the Basic header value, its base64 token and decoded `user:pass`, the
|
|
267
|
+
* `encodeURIComponent` forms; for the client also the `client_secret_basic` header the token
|
|
268
|
+
* request sends and the raw `id:secret`), and the stored OAuth client id (null without a
|
|
269
|
+
* client). Never echo the values; a plugin check gets only a manual client's id.
|
|
270
|
+
*/
|
|
271
|
+
async checkMaterial(orgId, connectorId) {
|
|
272
|
+
const prefix = orgKey(connectorId).slice(0, -"org".length);
|
|
273
|
+
const rows = await this.db
|
|
274
|
+
.selectFrom("secrets")
|
|
275
|
+
.select("key")
|
|
276
|
+
.where("namespace", "=", NAMESPACE)
|
|
277
|
+
.where("org_id", "=", orgId)
|
|
278
|
+
.where(sql `left(key, ${prefix.length})`, "=", prefix)
|
|
279
|
+
.execute();
|
|
280
|
+
const secrets = new Set();
|
|
281
|
+
const push = (...vs) => {
|
|
282
|
+
for (const v of vs) {
|
|
283
|
+
if (typeof v !== "string" || v === "")
|
|
284
|
+
continue;
|
|
285
|
+
// the value and the forms an injection carries it in (`Bearer`, query, percent-encoded)
|
|
286
|
+
for (const s of injectionSecrets({ headers: { a: `Bearer ${v}` }, query: { q: v } })) {
|
|
287
|
+
secrets.add(s);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
const pushBasic = (user, password) => {
|
|
292
|
+
const header = `Basic ${Buffer.from(`${user}:${password}`).toString("base64")}`;
|
|
293
|
+
for (const s of injectionSecrets({ headers: { a: header } }))
|
|
294
|
+
secrets.add(s);
|
|
295
|
+
};
|
|
296
|
+
let clientId = null;
|
|
297
|
+
for (const r of rows) {
|
|
298
|
+
const v = (await this.readJson(orgId, r.key, connectorId));
|
|
299
|
+
if (v === null || typeof v !== "object")
|
|
300
|
+
continue;
|
|
301
|
+
if (r.key === clientKey(connectorId)) {
|
|
302
|
+
// the client id is not a secret (a plugin check gets it); the secret and DCR token are
|
|
303
|
+
if (typeof v.clientId === "string")
|
|
304
|
+
clientId = v.clientId;
|
|
305
|
+
push(v.clientSecret, v.registration?.accessToken);
|
|
306
|
+
if (typeof v.clientId === "string" && typeof v.clientSecret === "string") {
|
|
307
|
+
// as the token request sends it (both parts form-encoded), and as typed
|
|
308
|
+
const header = basicClientAuth(v.clientId, v.clientSecret);
|
|
309
|
+
for (const s of injectionSecrets({ headers: { a: header } }))
|
|
310
|
+
secrets.add(s);
|
|
311
|
+
pushBasic(v.clientId, v.clientSecret);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
else if (typeof v.access_token === "string") {
|
|
315
|
+
push(v.access_token, v.refresh_token);
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
push(v.token, v.password);
|
|
319
|
+
if (typeof v.username === "string" && typeof v.password === "string") {
|
|
320
|
+
pushBasic(v.username, v.password);
|
|
321
|
+
}
|
|
322
|
+
if (typeof v.headers === "object" && v.headers !== null) {
|
|
323
|
+
const headers = Object.fromEntries(Object.entries(v.headers).filter((e) => typeof e[1] === "string"));
|
|
324
|
+
for (const s of injectionSecrets({ headers }))
|
|
325
|
+
secrets.add(s);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return { secrets: [...secrets], clientId };
|
|
330
|
+
}
|
|
242
331
|
/** 只看与 auth.ownership 对应的那一条凭据是否存在(不解密);none 恒为 connected。 */
|
|
243
332
|
async status(orgId, connectorId, auth, accountId) {
|
|
244
333
|
if (auth.type === "none")
|
|
@@ -75,6 +75,26 @@ export interface TokenRequestOptions {
|
|
|
75
75
|
/** More `token_type` values to accept as bearer tokens (`auth.tokenTypes`, case-insensitive). */
|
|
76
76
|
tokenTypes?: readonly string[] | undefined;
|
|
77
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* The headers and form of a token request (RFC 6749 §3.2): client_id / secret per the client's
|
|
80
|
+
* auth method (Basic form-encodes them first, §2.3.1; otherwise `client_id`, plus
|
|
81
|
+
* `client_secret` for `client_secret_post`). Empty parameters are left out.
|
|
82
|
+
*/
|
|
83
|
+
export declare function tokenRequestInit(client: OAuthClientRecord, params: Record<string, string | undefined>): {
|
|
84
|
+
headers: Record<string, string>;
|
|
85
|
+
body: URLSearchParams;
|
|
86
|
+
};
|
|
87
|
+
/** The form encoding of a Basic client credential part (RFC 6749 §2.3.1). */
|
|
88
|
+
export declare const formEncode: (s: string) => string;
|
|
89
|
+
/** The `Authorization` value of `client_secret_basic` (both parts form-encoded first). */
|
|
90
|
+
export declare const basicClientAuth: (clientId: string, clientSecret: string) => string;
|
|
91
|
+
/**
|
|
92
|
+
* The refusal code of a token endpoint answer, or null when it is not a refusal (the one parser of
|
|
93
|
+
* token refusals): a 400 / 401 whose JSON body has an RFC 6749-shaped `error`, or a 200 whose body
|
|
94
|
+
* carries `ok: false` (Slack) or an `error` and no `access_token`, with a code that is not
|
|
95
|
+
* transient. Only codes matching the RFC 6749 shape are returned, never other text of the body.
|
|
96
|
+
*/
|
|
97
|
+
export declare function parseTokenRefusal(status: number, body: unknown): string | null;
|
|
78
98
|
/**
|
|
79
99
|
* Calls the token endpoint (RFC 6749 §3.2), sending client_id / secret per the client's auth method
|
|
80
100
|
* (Basic form-encodes them first, §2.3.1).
|
|
@@ -68,20 +68,11 @@ const isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
|
68
68
|
/** RFC 6749 §5.2 形式的错误码(只有这种串才写进日志 / 错误,不回显对方任意文本)。 */
|
|
69
69
|
const OAUTH_ERROR_RE = /^[a-z][a-z0-9_]{0,63}$/;
|
|
70
70
|
/**
|
|
71
|
-
*
|
|
72
|
-
* (Basic form-encodes them first, §2.3.1
|
|
73
|
-
*
|
|
74
|
-
* - 200 with `ok: false` (Slack reports every error this way), or with an `error` code and no
|
|
75
|
-
* `access_token` -> OAuthGrantError with that code (`ok: false` wins over an `access_token`); a
|
|
76
|
-
* transient code (`temporarily_unavailable`, `server_error`, Slack's `ratelimited`, ...) or no
|
|
77
|
-
* parsable code -> OAuthUnavailable.
|
|
78
|
-
* - Everything else (network error, timeout, 429, 408, 403 and other 4xx, a 4xx without a code,
|
|
79
|
-
* 5xx, 3xx, a malformed body) -> OAuthUnavailable: a gateway's rate limit or a WAF block is not a
|
|
80
|
-
* dead credential.
|
|
81
|
-
* - A `token_type` other than Bearer / `user` / `opts.tokenTypes` -> OAuthGrantError
|
|
82
|
-
* `unsupported_token_type`; an accepted one is stored as `Bearer`.
|
|
71
|
+
* The headers and form of a token request (RFC 6749 §3.2): client_id / secret per the client's
|
|
72
|
+
* auth method (Basic form-encodes them first, §2.3.1; otherwise `client_id`, plus
|
|
73
|
+
* `client_secret` for `client_secret_post`). Empty parameters are left out.
|
|
83
74
|
*/
|
|
84
|
-
export
|
|
75
|
+
export function tokenRequestInit(client, params) {
|
|
85
76
|
const form = new URLSearchParams();
|
|
86
77
|
for (const [k, v] of Object.entries(params))
|
|
87
78
|
if (v !== undefined && v !== "")
|
|
@@ -91,8 +82,7 @@ export async function tokenRequest(client, params, http, opts = {}) {
|
|
|
91
82
|
accept: "application/json",
|
|
92
83
|
};
|
|
93
84
|
if (client.authMethod === "client_secret_basic" && client.clientSecret !== undefined) {
|
|
94
|
-
|
|
95
|
-
headers.authorization = `Basic ${Buffer.from(`${enc(client.clientId)}:${enc(client.clientSecret)}`).toString("base64")}`;
|
|
85
|
+
headers.authorization = basicClientAuth(client.clientId, client.clientSecret);
|
|
96
86
|
}
|
|
97
87
|
else {
|
|
98
88
|
form.set("client_id", client.clientId);
|
|
@@ -100,6 +90,46 @@ export async function tokenRequest(client, params, http, opts = {}) {
|
|
|
100
90
|
form.set("client_secret", client.clientSecret);
|
|
101
91
|
}
|
|
102
92
|
}
|
|
93
|
+
return { headers, body: form };
|
|
94
|
+
}
|
|
95
|
+
/** The form encoding of a Basic client credential part (RFC 6749 §2.3.1). */
|
|
96
|
+
export const formEncode = (s) => encodeURIComponent(s).replace(/%20/g, "+");
|
|
97
|
+
/** The `Authorization` value of `client_secret_basic` (both parts form-encoded first). */
|
|
98
|
+
export const basicClientAuth = (clientId, clientSecret) => `Basic ${Buffer.from(`${formEncode(clientId)}:${formEncode(clientSecret)}`).toString("base64")}`;
|
|
99
|
+
/**
|
|
100
|
+
* The refusal code of a token endpoint answer, or null when it is not a refusal (the one parser of
|
|
101
|
+
* token refusals): a 400 / 401 whose JSON body has an RFC 6749-shaped `error`, or a 200 whose body
|
|
102
|
+
* carries `ok: false` (Slack) or an `error` and no `access_token`, with a code that is not
|
|
103
|
+
* transient. Only codes matching the RFC 6749 shape are returned, never other text of the body.
|
|
104
|
+
*/
|
|
105
|
+
export function parseTokenRefusal(status, body) {
|
|
106
|
+
const code = isObj(body) ? body.error : undefined;
|
|
107
|
+
const shaped = typeof code === "string" && OAUTH_ERROR_RE.test(code) ? code : null;
|
|
108
|
+
if (status === 400 || status === 401)
|
|
109
|
+
return shaped;
|
|
110
|
+
if (status !== 200 || !isObj(body))
|
|
111
|
+
return null;
|
|
112
|
+
if (body.ok === false || (body.access_token === undefined && body.error !== undefined)) {
|
|
113
|
+
return shaped !== null && !TRANSIENT_ERRORS.has(shaped) ? shaped : null;
|
|
114
|
+
}
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Calls the token endpoint (RFC 6749 §3.2), sending client_id / secret per the client's auth method
|
|
119
|
+
* (Basic form-encodes them first, §2.3.1).
|
|
120
|
+
* - 400 / 401 with a parsable RFC 6749 `error` code -> OAuthGrantError.
|
|
121
|
+
* - 200 with `ok: false` (Slack reports every error this way), or with an `error` code and no
|
|
122
|
+
* `access_token` -> OAuthGrantError with that code (`ok: false` wins over an `access_token`); a
|
|
123
|
+
* transient code (`temporarily_unavailable`, `server_error`, Slack's `ratelimited`, ...) or no
|
|
124
|
+
* parsable code -> OAuthUnavailable.
|
|
125
|
+
* - Everything else (network error, timeout, 429, 408, 403 and other 4xx, a 4xx without a code,
|
|
126
|
+
* 5xx, 3xx, a malformed body) -> OAuthUnavailable: a gateway's rate limit or a WAF block is not a
|
|
127
|
+
* dead credential.
|
|
128
|
+
* - A `token_type` other than Bearer / `user` / `opts.tokenTypes` -> OAuthGrantError
|
|
129
|
+
* `unsupported_token_type`; an accepted one is stored as `Bearer`.
|
|
130
|
+
*/
|
|
131
|
+
export async function tokenRequest(client, params, http, opts = {}) {
|
|
132
|
+
const { headers, body: form } = tokenRequestInit(client, params);
|
|
103
133
|
let res;
|
|
104
134
|
try {
|
|
105
135
|
res = await oauthFetch(client.tokenEndpoint, { method: "POST", headers, body: form }, http);
|
|
@@ -116,18 +146,13 @@ export async function tokenRequest(client, params, http, opts = {}) {
|
|
|
116
146
|
catch {
|
|
117
147
|
body = undefined;
|
|
118
148
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
throw new OAuthGrantError(code);
|
|
123
|
-
}
|
|
149
|
+
const refusal = parseTokenRefusal(res.status, body);
|
|
150
|
+
if (refusal !== null)
|
|
151
|
+
throw new OAuthGrantError(refusal);
|
|
124
152
|
if (res.status !== 200 || !isObj(body))
|
|
125
153
|
throw new OAuthUnavailable();
|
|
154
|
+
// an error body without a usable code (or a transient one): unavailable, not a refusal
|
|
126
155
|
if (body.ok === false || (body.access_token === undefined && body.error !== undefined)) {
|
|
127
|
-
const code = body.error;
|
|
128
|
-
if (typeof code === "string" && OAUTH_ERROR_RE.test(code) && !TRANSIENT_ERRORS.has(code)) {
|
|
129
|
-
throw new OAuthGrantError(code);
|
|
130
|
-
}
|
|
131
156
|
throw new OAuthUnavailable();
|
|
132
157
|
}
|
|
133
158
|
const { access_token, refresh_token, expires_in, token_type, scope } = body;
|
package/dist/tools/pipeline.d.ts
CHANGED
|
@@ -151,8 +151,9 @@ interface PipelineDeps {
|
|
|
151
151
|
/** The org's switches of single extension tools: a turned-off one is not called (spec A §7). */
|
|
152
152
|
readonly switches: Pick<OrgToolSwitches, "isToolOff">;
|
|
153
153
|
/**
|
|
154
|
-
* Ids of an extension's `
|
|
155
|
-
* `ToolResult.card` must name one of them,
|
|
154
|
+
* Ids of an extension's `conversation.tool-result.card` contributions of any kind (view, embed
|
|
155
|
+
* or component; read live, no alias): a plugin tool's `ToolResult.card` must name one of them,
|
|
156
|
+
* otherwise it is dropped with a warning.
|
|
156
157
|
*/
|
|
157
158
|
inlineViews(plugin: string): ReadonlySet<string>;
|
|
158
159
|
/**
|
package/dist/ui/arrangement.d.ts
CHANGED
|
@@ -1,74 +1,65 @@
|
|
|
1
1
|
import { type UiSlotId } from "@capybara-hq/plugin-sdk";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import type { UiContributionItem } from "./views.js";
|
|
4
|
-
export
|
|
5
|
-
hidden:
|
|
6
|
-
order:
|
|
7
|
-
}
|
|
8
|
-
export type
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
"platform.page": "platform.page";
|
|
18
|
-
}> & z.core.$partial, z.ZodObject<{
|
|
19
|
-
hidden: z.ZodArray<z.ZodString>;
|
|
20
|
-
order: z.ZodArray<z.ZodString>;
|
|
21
|
-
}, z.core.$strict>>;
|
|
22
|
-
export type UiArrangement = z.infer<typeof UiArrangementSchema>;
|
|
4
|
+
export type UiListEntry = {
|
|
5
|
+
hidden: string[];
|
|
6
|
+
order: string[];
|
|
7
|
+
};
|
|
8
|
+
export type UiUseEntry = {
|
|
9
|
+
use: string | null;
|
|
10
|
+
};
|
|
11
|
+
/** One slot's entry: `{ hidden, order }` on a list slot, `{ use }` on a single slot. */
|
|
12
|
+
export type UiArrangementEntry = UiListEntry | UiUseEntry;
|
|
13
|
+
export type UiArrangement = Partial<Record<UiSlotId, UiArrangementEntry>>;
|
|
14
|
+
/** Whether a slot is a single (replaceable) slot. */
|
|
15
|
+
export declare const isSingle: (slot: UiSlotId) => boolean;
|
|
16
|
+
export declare const UiArrangementSchema: z.ZodType<UiArrangement>;
|
|
23
17
|
/** The slots an org arranges: every arrangeable slot but the platform's own. */
|
|
24
18
|
export declare const ORG_ARRANGEABLE: readonly UiSlotId[];
|
|
25
19
|
/** An org's patch: a slot's entry replaces the stored one, null removes it. */
|
|
26
|
-
export declare const OrgUiArrangementPatchSchema: z.
|
|
27
|
-
"agent.detail.section": "agent.detail.section";
|
|
28
|
-
"conversation.panel": "conversation.panel";
|
|
29
|
-
"conversation.properties.section": "conversation.properties.section";
|
|
30
|
-
"conversation.tool-result.card": "conversation.tool-result.card";
|
|
31
|
-
"file.preview": "file.preview";
|
|
32
|
-
"org.page": "org.page";
|
|
33
|
-
"org.settings.section": "org.settings.section";
|
|
34
|
-
"platform.page": "platform.page";
|
|
35
|
-
}> & z.core.$partial, z.ZodNullable<z.ZodObject<{
|
|
36
|
-
hidden: z.ZodArray<z.ZodString>;
|
|
37
|
-
order: z.ZodArray<z.ZodString>;
|
|
38
|
-
}, z.core.$strict>>>;
|
|
20
|
+
export declare const OrgUiArrangementPatchSchema: z.ZodType<Partial<Record<"agent.detail.section" | "conversation.composer.frame" | "conversation.empty" | "conversation.panel" | "conversation.properties.section" | "conversation.tool-result.card" | "file.preview" | "org.page" | "org.settings.section" | "platform.page" | "shell.brand" | "shell.overlay", UiArrangementEntry | null>>, unknown, z.core.$ZodTypeInternals<Partial<Record<"agent.detail.section" | "conversation.composer.frame" | "conversation.empty" | "conversation.panel" | "conversation.properties.section" | "conversation.tool-result.card" | "file.preview" | "org.page" | "org.settings.section" | "platform.page" | "shell.brand" | "shell.overlay", UiArrangementEntry | null>>, unknown>>;
|
|
39
21
|
/** A deployment patch: like {@link OrgUiArrangementPatchSchema} over every arrangeable slot. */
|
|
40
|
-
export declare const UiArrangementPatchSchema: z.
|
|
41
|
-
"agent.detail.section": "agent.detail.section";
|
|
42
|
-
"conversation.panel": "conversation.panel";
|
|
43
|
-
"conversation.properties.section": "conversation.properties.section";
|
|
44
|
-
"conversation.tool-result.card": "conversation.tool-result.card";
|
|
45
|
-
"file.preview": "file.preview";
|
|
46
|
-
"org.page": "org.page";
|
|
47
|
-
"org.settings.section": "org.settings.section";
|
|
48
|
-
"platform.page": "platform.page";
|
|
49
|
-
}> & z.core.$partial, z.ZodNullable<z.ZodObject<{
|
|
50
|
-
hidden: z.ZodArray<z.ZodString>;
|
|
51
|
-
order: z.ZodArray<z.ZodString>;
|
|
52
|
-
}, z.core.$strict>>>;
|
|
22
|
+
export declare const UiArrangementPatchSchema: z.ZodType<Partial<Record<"agent.detail.section" | "conversation.composer.frame" | "conversation.empty" | "conversation.panel" | "conversation.properties.section" | "conversation.tool-result.card" | "file.preview" | "org.page" | "org.settings.section" | "platform.page" | "shell.brand" | "shell.overlay", UiArrangementEntry | null>>, unknown, z.core.$ZodTypeInternals<Partial<Record<"agent.detail.section" | "conversation.composer.frame" | "conversation.empty" | "conversation.panel" | "conversation.properties.section" | "conversation.tool-result.card" | "file.preview" | "org.page" | "org.settings.section" | "platform.page" | "shell.brand" | "shell.overlay", UiArrangementEntry | null>>, unknown>>;
|
|
53
23
|
/** Merges a per-slot patch into the stored arrangement (null removes the slot's entry). */
|
|
54
24
|
export declare function mergeArrangement(current: UiArrangement | undefined, patch: Partial<Record<UiSlotId, UiArrangementEntry | null>>): UiArrangement;
|
|
55
|
-
/**
|
|
25
|
+
/**
|
|
26
|
+
* The list entry that applies to a slot: the org's if it arranged the slot, else the platform's
|
|
27
|
+
* (a `use` entry, which only a single slot stores, reads as none).
|
|
28
|
+
*/
|
|
56
29
|
export declare function effectiveEntry(org: UiArrangement | undefined, platform: UiArrangement, slot: UiSlotId): {
|
|
57
|
-
entry:
|
|
30
|
+
entry: UiListEntry;
|
|
58
31
|
source: "org" | "platform" | "none";
|
|
59
32
|
};
|
|
33
|
+
/**
|
|
34
|
+
* The winner of a single slot (client extensions spec §6.3): the org's entry if it has one, else
|
|
35
|
+
* the platform's, else none. No entry or `use: null` is the built-in (null); a `use` naming a key
|
|
36
|
+
* not in `available` (extension off in the org, contribution dropped, the viewer's role below the
|
|
37
|
+
* slot's) is the built-in too, never the other level's pick. A list slot, or an entry that is not
|
|
38
|
+
* a `use` entry, has no winner.
|
|
39
|
+
*/
|
|
40
|
+
export declare function resolveSingleSlot(slot: UiSlotId, orgEntry: UiArrangementEntry | undefined, platformEntry: UiArrangementEntry | undefined, available: ReadonlySet<string>): string | null;
|
|
41
|
+
/** The `use` stored for a single slot: the org's entry if it has one, else the platform's. */
|
|
42
|
+
export declare function storedUse(org: UiArrangement | undefined, platform: UiArrangement, slot: UiSlotId): {
|
|
43
|
+
use: string | null;
|
|
44
|
+
followsPlatform: boolean;
|
|
45
|
+
};
|
|
46
|
+
/** The arrangement key of a contribution: `<plugin>/<id>`. */
|
|
47
|
+
export declare const keyOf: (i: {
|
|
48
|
+
plugin: string;
|
|
49
|
+
id: string;
|
|
50
|
+
}) => string;
|
|
60
51
|
/**
|
|
61
52
|
* Orders the items of one slot (hidden ones stay in place): grouped by anchor in the slot's anchor
|
|
62
53
|
* order, also without an entry (the lists match what the page renders per anchor); within a group
|
|
63
54
|
* the keys of `entry.order` come first in that order, the rest in base order. Unknown keys are
|
|
64
55
|
* ignored, so an order never moves an item across anchors.
|
|
65
56
|
*/
|
|
66
|
-
export declare function orderItems(items: UiContributionItem[], entry:
|
|
57
|
+
export declare function orderItems(items: UiContributionItem[], entry: UiListEntry): UiContributionItem[];
|
|
67
58
|
/** {@link orderItems} without the hidden keys. */
|
|
68
|
-
export declare function arrange(items: UiContributionItem[], entry:
|
|
59
|
+
export declare function arrange(items: UiContributionItem[], entry: UiListEntry): UiContributionItem[];
|
|
69
60
|
/**
|
|
70
61
|
* Applies the per-slot entries to a mixed list: each slot's items are arranged and written back
|
|
71
62
|
* into the positions that slot occupied (a slot without an entry: its base order, grouped by
|
|
72
63
|
* anchor).
|
|
73
64
|
*/
|
|
74
|
-
export declare function arrangeBySlot(items: UiContributionItem[], entryOf: (slot: UiSlotId) =>
|
|
65
|
+
export declare function arrangeBySlot(items: UiContributionItem[], entryOf: (slot: UiSlotId) => UiListEntry): UiContributionItem[];
|
package/dist/ui/arrangement.js
CHANGED
|
@@ -1,22 +1,66 @@
|
|
|
1
|
-
import { UI_ARRANGEABLE_SLOTS, UI_CONTRIBUTION_KEY_RE, uiSlot, } from "@capybara-hq/plugin-sdk";
|
|
1
|
+
import { UI_ARRANGEABLE_SLOTS, UI_CONTRIBUTION_KEY_RE, UI_SINGLE_SLOTS, uiSlot, } from "@capybara-hq/plugin-sdk";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
/**
|
|
4
|
-
* Arrangement of UI slots (UI slots spec §5): per slot, which
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Arrangement of UI slots (UI slots spec §5, client extensions spec §6.3): per list slot, which
|
|
5
|
+
* contributions are hidden and in which order; per single slot, which contribution replaces the
|
|
6
|
+
* built-in (`use`, null = the built-in). "Hidden" means not listed and `use` only picks what is
|
|
7
|
+
* listed: the access gate never reads an arrangement, so a contribution's view, action and embed
|
|
8
|
+
* routes keep working for an allowed role whatever the arrangement says.
|
|
7
9
|
*/
|
|
8
|
-
const Key = z.string().regex(UI_CONTRIBUTION_KEY_RE);
|
|
10
|
+
const Key = z.string().regex(UI_CONTRIBUTION_KEY_RE, "must be a contribution key <plugin>/<id>");
|
|
9
11
|
const LIST_MAX = 200;
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
/**
|
|
13
|
+
* A list slot's entry: the keys hidden and the keys ordered first. The refusals name the slot and
|
|
14
|
+
* what it takes (actionable for an admin and the Platform tool's model).
|
|
15
|
+
*/
|
|
16
|
+
const listEntrySchema = (slot) => {
|
|
17
|
+
const keys = z
|
|
18
|
+
.array(Key, {
|
|
19
|
+
error: (iss) => iss.input === undefined
|
|
20
|
+
? `${slot} is a list slot: give an array of contribution keys <plugin>/<id>`
|
|
21
|
+
: undefined,
|
|
22
|
+
})
|
|
23
|
+
.max(LIST_MAX);
|
|
24
|
+
return z.strictObject({ hidden: keys, order: keys }, {
|
|
25
|
+
error: (iss) => iss.code === "unrecognized_keys"
|
|
26
|
+
? `${slot} is a list slot: give { hidden, order }; use is only for the single slots (${UI_SINGLE_SLOTS.join(", ")})`
|
|
27
|
+
: undefined,
|
|
28
|
+
});
|
|
29
|
+
};
|
|
30
|
+
/** A single slot's entry: the contribution that replaces the built-in, or null for the built-in. */
|
|
31
|
+
const useEntrySchema = (slot) => z.strictObject({
|
|
32
|
+
use: z.union([Key, z.null()], {
|
|
33
|
+
error: (iss) => iss.input === undefined
|
|
34
|
+
? `${slot} needs use: a contribution key <plugin>/<id>, or null for the built-in`
|
|
35
|
+
: undefined,
|
|
36
|
+
}),
|
|
37
|
+
}, {
|
|
38
|
+
error: (iss) => iss.code === "unrecognized_keys"
|
|
39
|
+
? `${slot} is a single slot: give { use } (a contribution key <plugin>/<id>, or null for the built-in); hidden and order are only for list slots`
|
|
40
|
+
: undefined,
|
|
41
|
+
});
|
|
42
|
+
/** Whether a slot is a single (replaceable) slot. */
|
|
43
|
+
export const isSingle = (slot) => UI_SINGLE_SLOTS.includes(slot);
|
|
44
|
+
/**
|
|
45
|
+
* An object over `slots`, each optional and of its kind's entry (nullable in a patch); unknown
|
|
46
|
+
* slots are refused, and so are `use` on a list slot and `hidden` / `order` on a single slot (400
|
|
47
|
+
* `validation_failed` on the settings routes).
|
|
48
|
+
*/
|
|
49
|
+
function arrangementShape(slots, nullable) {
|
|
50
|
+
const shape = {};
|
|
51
|
+
for (const slot of slots) {
|
|
52
|
+
const entry = isSingle(slot) ? useEntrySchema(slot) : listEntrySchema(slot);
|
|
53
|
+
shape[slot] = (nullable ? entry.nullable() : entry).optional();
|
|
54
|
+
}
|
|
55
|
+
return z.object(shape).strict();
|
|
56
|
+
}
|
|
57
|
+
export const UiArrangementSchema = arrangementShape(UI_ARRANGEABLE_SLOTS, false);
|
|
14
58
|
/** The slots an org arranges: every arrangeable slot but the platform's own. */
|
|
15
59
|
export const ORG_ARRANGEABLE = UI_ARRANGEABLE_SLOTS.filter((s) => s !== "platform.page");
|
|
16
60
|
/** An org's patch: a slot's entry replaces the stored one, null removes it. */
|
|
17
|
-
export const OrgUiArrangementPatchSchema =
|
|
61
|
+
export const OrgUiArrangementPatchSchema = arrangementShape(ORG_ARRANGEABLE, true);
|
|
18
62
|
/** A deployment patch: like {@link OrgUiArrangementPatchSchema} over every arrangeable slot. */
|
|
19
|
-
export const UiArrangementPatchSchema =
|
|
63
|
+
export const UiArrangementPatchSchema = arrangementShape(UI_ARRANGEABLE_SLOTS, true);
|
|
20
64
|
/** Merges a per-slot patch into the stored arrangement (null removes the slot's entry). */
|
|
21
65
|
export function mergeArrangement(current, patch) {
|
|
22
66
|
const merged = { ...current };
|
|
@@ -29,17 +73,46 @@ export function mergeArrangement(current, patch) {
|
|
|
29
73
|
return merged;
|
|
30
74
|
}
|
|
31
75
|
const NONE = { hidden: [], order: [] };
|
|
32
|
-
|
|
76
|
+
const asList = (e) => ("hidden" in e ? e : NONE);
|
|
77
|
+
/**
|
|
78
|
+
* The list entry that applies to a slot: the org's if it arranged the slot, else the platform's
|
|
79
|
+
* (a `use` entry, which only a single slot stores, reads as none).
|
|
80
|
+
*/
|
|
33
81
|
export function effectiveEntry(org, platform, slot) {
|
|
34
82
|
const o = org?.[slot];
|
|
35
83
|
if (o !== undefined)
|
|
36
|
-
return { entry: o, source: "org" };
|
|
84
|
+
return { entry: asList(o), source: "org" };
|
|
37
85
|
const p = platform[slot];
|
|
38
86
|
if (p !== undefined)
|
|
39
|
-
return { entry: p, source: "platform" };
|
|
87
|
+
return { entry: asList(p), source: "platform" };
|
|
40
88
|
return { entry: NONE, source: "none" };
|
|
41
89
|
}
|
|
42
|
-
|
|
90
|
+
/**
|
|
91
|
+
* The winner of a single slot (client extensions spec §6.3): the org's entry if it has one, else
|
|
92
|
+
* the platform's, else none. No entry or `use: null` is the built-in (null); a `use` naming a key
|
|
93
|
+
* not in `available` (extension off in the org, contribution dropped, the viewer's role below the
|
|
94
|
+
* slot's) is the built-in too, never the other level's pick. A list slot, or an entry that is not
|
|
95
|
+
* a `use` entry, has no winner.
|
|
96
|
+
*/
|
|
97
|
+
export function resolveSingleSlot(slot, orgEntry, platformEntry, available) {
|
|
98
|
+
if (!isSingle(slot))
|
|
99
|
+
return null;
|
|
100
|
+
const entry = orgEntry ?? platformEntry;
|
|
101
|
+
if (entry === undefined || !("use" in entry) || entry.use === null)
|
|
102
|
+
return null;
|
|
103
|
+
return available.has(entry.use) ? entry.use : null;
|
|
104
|
+
}
|
|
105
|
+
/** The `use` stored for a single slot: the org's entry if it has one, else the platform's. */
|
|
106
|
+
export function storedUse(org, platform, slot) {
|
|
107
|
+
const o = org?.[slot];
|
|
108
|
+
const e = o ?? platform[slot];
|
|
109
|
+
return {
|
|
110
|
+
use: e !== undefined && "use" in e ? e.use : null,
|
|
111
|
+
followsPlatform: o === undefined,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/** The arrangement key of a contribution: `<plugin>/<id>`. */
|
|
115
|
+
export const keyOf = (i) => `${i.plugin}/${i.id}`;
|
|
43
116
|
/**
|
|
44
117
|
* Orders the items of one slot (hidden ones stay in place): grouped by anchor in the slot's anchor
|
|
45
118
|
* order, also without an entry (the lists match what the page renders per anchor); within a group
|
package/dist/ui/registry.d.ts
CHANGED
|
@@ -45,19 +45,29 @@ export declare const uiFrame: (m: UiBusMessage) => string;
|
|
|
45
45
|
/**
|
|
46
46
|
* 单个插件在 setup 期间注册的视图与动作:进程内的静态登记(同 `ctx.tools.register`),
|
|
47
47
|
* 不是跨请求的共享状态——各副本加载同一插件得到同样的登记。
|
|
48
|
-
* id
|
|
48
|
+
* The id must be in `permissions.ui.views` or `permissions.ui.components` (a component's server
|
|
49
|
+
* view, client extensions spec §5.2); `defineExtension` already cross-checked them with the
|
|
50
|
+
* manifest `ui`.
|
|
49
51
|
*/
|
|
50
52
|
export declare class PluginUiRegistration {
|
|
51
53
|
private readonly manifest;
|
|
52
|
-
/**
|
|
54
|
+
/**
|
|
55
|
+
* Out of scope (the id is in neither `permissions.ui.views` nor `permissions.ui.components`):
|
|
56
|
+
* throws ExtensionPermissionError and writes an audit entry.
|
|
57
|
+
*/
|
|
53
58
|
private readonly deny;
|
|
54
59
|
readonly views: Map<string, UiRender>;
|
|
55
60
|
readonly actions: Map<string, UiActionHandler>;
|
|
56
61
|
private readonly declaredViews;
|
|
57
62
|
private readonly contributions;
|
|
63
|
+
/** The ids of the manifest's `component` contributions: their permission is `ui.components`. */
|
|
64
|
+
private readonly components;
|
|
58
65
|
private sealed;
|
|
59
66
|
constructor(manifest: ExtensionManifest,
|
|
60
|
-
/**
|
|
67
|
+
/**
|
|
68
|
+
* Out of scope (the id is in neither `permissions.ui.views` nor `permissions.ui.components`):
|
|
69
|
+
* throws ExtensionPermissionError and writes an audit entry.
|
|
70
|
+
*/
|
|
61
71
|
deny: (item: string) => never);
|
|
62
72
|
/** setup 结束后调用:此后 view / action 抛错。 */
|
|
63
73
|
seal(): void;
|
package/dist/ui/registry.js
CHANGED
|
@@ -31,7 +31,9 @@ export const uiFrame = (m) => `event: ui\ndata: ${JSON.stringify({ plugin: m.plu
|
|
|
31
31
|
/**
|
|
32
32
|
* 单个插件在 setup 期间注册的视图与动作:进程内的静态登记(同 `ctx.tools.register`),
|
|
33
33
|
* 不是跨请求的共享状态——各副本加载同一插件得到同样的登记。
|
|
34
|
-
* id
|
|
34
|
+
* The id must be in `permissions.ui.views` or `permissions.ui.components` (a component's server
|
|
35
|
+
* view, client extensions spec §5.2); `defineExtension` already cross-checked them with the
|
|
36
|
+
* manifest `ui`.
|
|
35
37
|
*/
|
|
36
38
|
export class PluginUiRegistration {
|
|
37
39
|
manifest;
|
|
@@ -40,14 +42,21 @@ export class PluginUiRegistration {
|
|
|
40
42
|
actions = new Map();
|
|
41
43
|
declaredViews;
|
|
42
44
|
contributions;
|
|
45
|
+
/** The ids of the manifest's `component` contributions: their permission is `ui.components`. */
|
|
46
|
+
components;
|
|
43
47
|
sealed = false;
|
|
44
48
|
constructor(manifest,
|
|
45
|
-
/**
|
|
49
|
+
/**
|
|
50
|
+
* Out of scope (the id is in neither `permissions.ui.views` nor `permissions.ui.components`):
|
|
51
|
+
* throws ExtensionPermissionError and writes an audit entry.
|
|
52
|
+
*/
|
|
46
53
|
deny) {
|
|
47
54
|
this.manifest = manifest;
|
|
48
55
|
this.deny = deny;
|
|
49
|
-
|
|
56
|
+
const ui = manifest.permissions?.ui;
|
|
57
|
+
this.declaredViews = new Set([...(ui?.views ?? []), ...(ui?.components ?? [])]);
|
|
50
58
|
this.contributions = new Set((manifest.ui ?? []).map((c) => c.id));
|
|
59
|
+
this.components = new Set((manifest.ui ?? []).filter((c) => c.kind === "component").map((c) => c.id));
|
|
51
60
|
}
|
|
52
61
|
/** setup 结束后调用:此后 view / action 抛错。 */
|
|
53
62
|
seal() {
|
|
@@ -99,7 +108,9 @@ export class PluginUiRegistration {
|
|
|
99
108
|
throw new Error(`plugin ${name}: ui.${what} is only allowed during setup; setup has completed`);
|
|
100
109
|
}
|
|
101
110
|
if (typeof id !== "string" || !this.declaredViews.has(id)) {
|
|
102
|
-
|
|
111
|
+
// name the list the id belongs in: a component's server view is declared under components
|
|
112
|
+
const list = typeof id === "string" && this.components.has(id) ? "components" : "views";
|
|
113
|
+
this.deny(`permissions.ui.${list} ${String(id)}`);
|
|
103
114
|
}
|
|
104
115
|
if (typeof fn !== "function") {
|
|
105
116
|
throw new Error(`plugin ${name}: ui.${what} "${id}" handler must be a function`);
|
package/dist/ui/ui.schemas.d.ts
CHANGED
|
@@ -9,6 +9,8 @@ export declare const ContributionParams: z.ZodObject<{
|
|
|
9
9
|
export declare const ContributionsQuery: z.ZodObject<{
|
|
10
10
|
slot: z.ZodOptional<z.ZodEnum<{
|
|
11
11
|
"agent.detail.section": "agent.detail.section";
|
|
12
|
+
"conversation.composer.frame": "conversation.composer.frame";
|
|
13
|
+
"conversation.empty": "conversation.empty";
|
|
12
14
|
"conversation.panel": "conversation.panel";
|
|
13
15
|
"conversation.properties.section": "conversation.properties.section";
|
|
14
16
|
"conversation.tool-result.card": "conversation.tool-result.card";
|
|
@@ -16,6 +18,8 @@ export declare const ContributionsQuery: z.ZodObject<{
|
|
|
16
18
|
"org.page": "org.page";
|
|
17
19
|
"org.settings.section": "org.settings.section";
|
|
18
20
|
"platform.page": "platform.page";
|
|
21
|
+
"shell.brand": "shell.brand";
|
|
22
|
+
"shell.overlay": "shell.overlay";
|
|
19
23
|
}>>;
|
|
20
24
|
}, z.core.$strip>;
|
|
21
25
|
export declare const ViewQuery: z.ZodObject<{
|