@holokai/harbor-sdk 0.5.0 → 0.6.0-rc.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/build-inputs.json +3 -3
- package/dist/manifest.d.ts +30 -1
- package/dist/manifest.js +22 -0
- package/dist/package.d.ts +25 -5
- package/dist/package.js +89 -4
- package/package.json +1 -1
package/dist/build-inputs.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"package": "@holokai/harbor-sdk",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"digest": "
|
|
5
|
-
"commit": "
|
|
3
|
+
"version": "0.6.0-rc.0",
|
|
4
|
+
"digest": "ee14f279ce9678445cfa96aca7c3c14e5a2ed36ad5f7ba0586d49f3a61aa36a8",
|
|
5
|
+
"commit": "86e86d6f6f6131ff2d8bc9117e165d3f9000f8a2"
|
|
6
6
|
}
|
package/dist/manifest.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** The manifest
|
|
1
|
+
/** The manifest a first-party extension exports and harbor reads, and the helpers that declare its sign-in client. */
|
|
2
2
|
import type { DeclaredGrant } from './contract';
|
|
3
3
|
/** An image for an extension, the MCP `Icon` shape; `src` is a data: URL. */
|
|
4
4
|
export interface ExtensionIcon {
|
|
@@ -6,6 +6,18 @@ export interface ExtensionIcon {
|
|
|
6
6
|
mimeType?: string;
|
|
7
7
|
sizes?: string[];
|
|
8
8
|
}
|
|
9
|
+
/**
|
|
10
|
+
* Where a provider's confidential client trades a code: the one address an organization's client secret is sent to.
|
|
11
|
+
* Every extension of one provider declares the same one.
|
|
12
|
+
*/
|
|
13
|
+
export interface SignInDeclaration {
|
|
14
|
+
/** The vendor's token endpoint, https. */
|
|
15
|
+
token: string;
|
|
16
|
+
/** The setting holding the client id, one of `settings`. */
|
|
17
|
+
clientId: string;
|
|
18
|
+
/** The setting holding the client secret, one of `secretSettings`. */
|
|
19
|
+
clientSecret: string;
|
|
20
|
+
}
|
|
9
21
|
/** Who an extension is: what harbor shows for it and what it needs to run. */
|
|
10
22
|
export interface ExtensionManifest {
|
|
11
23
|
/** The path segment it is served at, lowercase letters, digits and dashes, starting with a letter. */
|
|
@@ -21,6 +33,23 @@ export interface ExtensionManifest {
|
|
|
21
33
|
icon?: ExtensionIcon;
|
|
22
34
|
/** The names of the settings it reads from its environment, such as `MICROSOFT_CLIENT_ID`. */
|
|
23
35
|
settings: readonly string[];
|
|
36
|
+
/** Which of `settings` are secret: shown masked, and never logged. */
|
|
37
|
+
secretSettings?: readonly string[];
|
|
38
|
+
/** A label for each setting, for an organization's admin. */
|
|
39
|
+
settingLabels?: Readonly<Record<string, string>>;
|
|
40
|
+
/** Where its sign-in client trades a code, for a provider with a confidential client. */
|
|
41
|
+
signIn?: SignInDeclaration;
|
|
24
42
|
/** The grants harbor gives only to an extension that names them, such as `files.add`. */
|
|
25
43
|
grants?: readonly DeclaredGrant[];
|
|
26
44
|
}
|
|
45
|
+
/** One field of a provider's sign-in client, as the manifest needs it: the kit's `ClientField` fits. */
|
|
46
|
+
export interface ManifestClientField {
|
|
47
|
+
name: string;
|
|
48
|
+
label: string;
|
|
49
|
+
secret: boolean;
|
|
50
|
+
env: string;
|
|
51
|
+
}
|
|
52
|
+
/** The manifest's settings for a provider's client: their names, which are secret, and their labels. */
|
|
53
|
+
export declare function clientSettings(fields: readonly ManifestClientField[]): Pick<ExtensionManifest, 'settings' | 'secretSettings' | 'settingLabels'>;
|
|
54
|
+
/** A confidential client's sign-in: `token`, and the settings of its `clientId` and `clientSecret` fields. */
|
|
55
|
+
export declare function signInDeclaration(token: string, fields: readonly ManifestClientField[]): SignInDeclaration;
|
package/dist/manifest.js
CHANGED
|
@@ -1 +1,23 @@
|
|
|
1
1
|
import {createRequire as __harborCreateRequire} from 'node:module'; const require = __harborCreateRequire(import.meta.url);
|
|
2
|
+
|
|
3
|
+
// src/manifest.ts
|
|
4
|
+
function clientSettings(fields) {
|
|
5
|
+
const secret = fields.filter((field) => field.secret).map((field) => field.env);
|
|
6
|
+
return {
|
|
7
|
+
settings: fields.map((field) => field.env),
|
|
8
|
+
...secret.length === 0 ? {} : { secretSettings: secret },
|
|
9
|
+
settingLabels: Object.fromEntries(fields.map((field) => [field.env, field.label]))
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
function signInDeclaration(token, fields) {
|
|
13
|
+
const env = (name, secret) => {
|
|
14
|
+
const field = fields.find((candidate) => candidate.name === name && candidate.secret === secret);
|
|
15
|
+
if (field === void 0) throw new Error(`a sign-in client needs a ${secret ? "secret " : ""}${name} field`);
|
|
16
|
+
return field.env;
|
|
17
|
+
};
|
|
18
|
+
return { token, clientId: env("clientId", false), clientSecret: env("clientSecret", true) };
|
|
19
|
+
}
|
|
20
|
+
export {
|
|
21
|
+
clientSettings,
|
|
22
|
+
signInDeclaration
|
|
23
|
+
};
|
package/dist/package.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* It imports nothing at run time, so Harbor's main process, the extension host and the build scripts all read it.
|
|
4
4
|
*/
|
|
5
5
|
import type { DeclaredGrant } from './contract';
|
|
6
|
-
import type { ExtensionManifest } from './manifest';
|
|
6
|
+
import type { ExtensionIcon, ExtensionManifest, SignInDeclaration } from './manifest';
|
|
7
7
|
/** The extension-to-host API version. A host runs only extensions that declare it. */
|
|
8
8
|
export declare const EXTENSION_API_VERSION = 1;
|
|
9
9
|
/** The `harbor` field of a published package's package.json: which kind of package it is. */
|
|
@@ -119,12 +119,23 @@ export type ReleaseTarget = (typeof RELEASE_TARGETS)[number];
|
|
|
119
119
|
/** The lanes a release manifest is published on. */
|
|
120
120
|
export declare const RELEASE_LANES: readonly ["production", "beta"];
|
|
121
121
|
export type ReleaseLane = (typeof RELEASE_LANES)[number];
|
|
122
|
-
/**
|
|
122
|
+
/**
|
|
123
|
+
* One extension a release set holds. The fields after `grants` are optional, so a record without them, older or
|
|
124
|
+
* newer, still parses; an organization's Moku reads them to manage the extension's settings.
|
|
125
|
+
*/
|
|
123
126
|
export interface ReleaseSetExtension {
|
|
124
127
|
version: string;
|
|
125
128
|
/** The provider decides the process, and so the shared sign-in, the extension runs in. */
|
|
126
129
|
provider: string;
|
|
127
130
|
grants: readonly DeclaredGrant[];
|
|
131
|
+
title?: string;
|
|
132
|
+
description?: string;
|
|
133
|
+
/** The extension's own icon, from its manifest, so a reader shows it without opening an archive. */
|
|
134
|
+
icon?: ExtensionIcon;
|
|
135
|
+
settings?: readonly string[];
|
|
136
|
+
secretSettings?: readonly string[];
|
|
137
|
+
settingLabels?: Readonly<Record<string, string>>;
|
|
138
|
+
signIn?: SignInDeclaration;
|
|
128
139
|
}
|
|
129
140
|
/** One target's archive of a release set. */
|
|
130
141
|
export interface ReleaseArtifact {
|
|
@@ -185,6 +196,8 @@ export declare const laneManifestKey: (lane: ReleaseLane, sequence: number) => s
|
|
|
185
196
|
export declare const laneSignatureKey: (lane: ReleaseLane, sequence: number) => string;
|
|
186
197
|
/** The store key of a lane's pointer, the one object in the store that is replaced. */
|
|
187
198
|
export declare const lanePointerKey: (lane: ReleaseLane) => string;
|
|
199
|
+
/** A set record's entry for an extension, from its installed manifest and version. */
|
|
200
|
+
export declare function setRecordExtension(manifest: ExtensionManifest, version: string): ReleaseSetExtension;
|
|
188
201
|
/** A set record, or null when any field is out of shape. Only ReleaseSet's fields survive. */
|
|
189
202
|
export declare function parseReleaseSet(value: unknown): ReleaseSet | null;
|
|
190
203
|
/** A lane manifest, or null when it or any set it lists is out of shape, or it lists one set id twice. */
|
|
@@ -221,15 +234,22 @@ export declare function incompatibility(set: ReleaseSet, here: {
|
|
|
221
234
|
running: Record<string, string>;
|
|
222
235
|
}): Incompatibility | null;
|
|
223
236
|
/** Why a release policy refuses a set that is protocol-compatible. */
|
|
224
|
-
export type PolicyRefusal = 'new_extension' | 'removed_extension' | 'changes_provider' | 'organization';
|
|
237
|
+
export type PolicyRefusal = 'new_extension' | 'removed_extension' | 'changes_provider' | 'changes_sign_in_authority' | 'sign_ins_disagree' | 'organization';
|
|
225
238
|
/** Rollout and organization policy, apart from protocol. */
|
|
226
239
|
export interface ExtensionReleasePolicy {
|
|
227
240
|
/** The reason the set may not replace the running extensions, or null when it may. */
|
|
228
241
|
refusal(set: ReleaseSet, running: ReleaseSet['extensions']): PolicyRefusal | null;
|
|
229
242
|
}
|
|
230
243
|
/**
|
|
231
|
-
*
|
|
232
|
-
* provider
|
|
244
|
+
* Whether a set's sign-ins agree: every extension of one provider declares the same `signIn`, since a provider is
|
|
245
|
+
* one process and one sign-in, and a setting name belongs to one provider, so an organization's value for it reaches
|
|
246
|
+
* one sign-in. The publisher refuses to build a set that breaks this, and Harbor refuses to install one.
|
|
247
|
+
*/
|
|
248
|
+
export declare function signInsAgree(extensions: Readonly<Record<string, ReleaseSetExtension>>): boolean;
|
|
249
|
+
/**
|
|
250
|
+
* The first-party default: a set that adds an extension, drops one the running tree holds, moves one to another
|
|
251
|
+
* provider, or changes where one's sign-in client trades a code (the address an organization's client secret goes
|
|
252
|
+
* to) waits for an app release. A set whose sign-ins disagree is never installed.
|
|
233
253
|
*/
|
|
234
254
|
export declare const FIRST_PARTY_ROLLOUT: ExtensionReleasePolicy;
|
|
235
255
|
/** The grants of `declared` that `authorized` holds, by stable id; the rest are withheld. */
|
package/dist/package.js
CHANGED
|
@@ -28,6 +28,36 @@ function parseIcon(value) {
|
|
|
28
28
|
...value.sizes === void 0 ? {} : { sizes: [...value.sizes] }
|
|
29
29
|
};
|
|
30
30
|
}
|
|
31
|
+
function parseSettingFields(settings, value) {
|
|
32
|
+
const { secretSettings, settingLabels, signIn } = value;
|
|
33
|
+
if (secretSettings !== void 0 && !(isStringArray(secretSettings) && secretSettings.every((n) => settings.includes(n)))) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
if (settingLabels !== void 0 && !(isRecord(settingLabels) && Object.entries(settingLabels).every(([n, l]) => settings.includes(n) && isString(l)))) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
let parsedSignIn;
|
|
40
|
+
if (signIn !== void 0) {
|
|
41
|
+
if (!isRecord(signIn) || !isString(signIn.token) || !isString(signIn.clientId) || !isString(signIn.clientSecret)) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
if (!isHttps(signIn.token) || !settings.includes(signIn.clientId)) return null;
|
|
45
|
+
if (!secretSettings?.includes(signIn.clientSecret)) return null;
|
|
46
|
+
parsedSignIn = { token: signIn.token, clientId: signIn.clientId, clientSecret: signIn.clientSecret };
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
...secretSettings === void 0 ? {} : { secretSettings: [...secretSettings] },
|
|
50
|
+
...settingLabels === void 0 ? {} : { settingLabels: { ...settingLabels } },
|
|
51
|
+
...parsedSignIn === void 0 ? {} : { signIn: parsedSignIn }
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function isHttps(value) {
|
|
55
|
+
try {
|
|
56
|
+
return new URL(value).protocol === "https:";
|
|
57
|
+
} catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
31
61
|
function parseExtensionManifest(value) {
|
|
32
62
|
if (!isRecord(value)) return null;
|
|
33
63
|
const { name, title, description, version, provider, icon, settings, grants } = value;
|
|
@@ -40,6 +70,8 @@ function parseExtensionManifest(value) {
|
|
|
40
70
|
if (grants !== void 0 && !(isStringArray(grants) && grants.every((grant) => DECLARED_GRANTS.has(grant)))) {
|
|
41
71
|
return null;
|
|
42
72
|
}
|
|
73
|
+
const settingFields = parseSettingFields(settings, value);
|
|
74
|
+
if (settingFields === null) return null;
|
|
43
75
|
return {
|
|
44
76
|
name,
|
|
45
77
|
title,
|
|
@@ -48,6 +80,7 @@ function parseExtensionManifest(value) {
|
|
|
48
80
|
provider,
|
|
49
81
|
...parsedIcon === void 0 ? {} : { icon: parsedIcon },
|
|
50
82
|
settings: [...settings],
|
|
83
|
+
...settingFields,
|
|
51
84
|
...grants === void 0 ? {} : { grants: [...grants] }
|
|
52
85
|
};
|
|
53
86
|
}
|
|
@@ -164,10 +197,39 @@ var laneSignatureKey = (lane, sequence) => `${laneManifestKey(lane, sequence)}.s
|
|
|
164
197
|
var lanePointerKey = (lane) => `extensions/lanes/${lane}/latest.json`;
|
|
165
198
|
function parseSetExtension(value) {
|
|
166
199
|
if (!isRecord(value)) return null;
|
|
167
|
-
const { version, provider, grants } = value;
|
|
200
|
+
const { version, provider, grants, title, description, icon, settings } = value;
|
|
168
201
|
if (!isString(version) || !isVersion(version) || !isString(provider) || !NAME.test(provider)) return null;
|
|
169
202
|
if (!isStringArray(grants) || !grants.every((grant) => DECLARED_GRANTS.has(grant))) return null;
|
|
170
|
-
|
|
203
|
+
if (title !== void 0 && !isString(title) || description !== void 0 && !isString(description)) return null;
|
|
204
|
+
const parsedIcon = icon === void 0 ? void 0 : parseIcon(icon);
|
|
205
|
+
if (parsedIcon === null) return null;
|
|
206
|
+
if (settings !== void 0 && !isStringArray(settings)) return null;
|
|
207
|
+
const settingFields = parseSettingFields(settings ?? [], value);
|
|
208
|
+
if (settingFields === null) return null;
|
|
209
|
+
return {
|
|
210
|
+
version,
|
|
211
|
+
provider,
|
|
212
|
+
grants: [...grants],
|
|
213
|
+
...title === void 0 ? {} : { title },
|
|
214
|
+
...description === void 0 ? {} : { description },
|
|
215
|
+
...parsedIcon === void 0 ? {} : { icon: parsedIcon },
|
|
216
|
+
...settings === void 0 ? {} : { settings: [...settings] },
|
|
217
|
+
...settingFields
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
function setRecordExtension(manifest, version) {
|
|
221
|
+
return {
|
|
222
|
+
version,
|
|
223
|
+
provider: manifest.provider,
|
|
224
|
+
grants: [...manifest.grants ?? []],
|
|
225
|
+
title: manifest.title,
|
|
226
|
+
description: manifest.description,
|
|
227
|
+
...manifest.icon === void 0 ? {} : { icon: { ...manifest.icon } },
|
|
228
|
+
settings: [...manifest.settings],
|
|
229
|
+
...manifest.secretSettings === void 0 ? {} : { secretSettings: [...manifest.secretSettings] },
|
|
230
|
+
...manifest.settingLabels === void 0 ? {} : { settingLabels: { ...manifest.settingLabels } },
|
|
231
|
+
...manifest.signIn === void 0 ? {} : { signIn: { ...manifest.signIn } }
|
|
232
|
+
};
|
|
171
233
|
}
|
|
172
234
|
function parseArtifact(value, id, target) {
|
|
173
235
|
if (!isRecord(value)) return null;
|
|
@@ -290,12 +352,33 @@ function incompatibility(set, here) {
|
|
|
290
352
|
if (set.artifacts[here.target] === void 0) return "no_artifact";
|
|
291
353
|
return null;
|
|
292
354
|
}
|
|
355
|
+
var sameSignIn = (a, b) => a?.token === b?.token && a?.clientId === b?.clientId && a?.clientSecret === b?.clientSecret;
|
|
356
|
+
function signInsAgree(extensions) {
|
|
357
|
+
const signIns = /* @__PURE__ */ new Map();
|
|
358
|
+
const owners = /* @__PURE__ */ new Map();
|
|
359
|
+
for (const extension of Object.values(extensions)) {
|
|
360
|
+
if (signIns.has(extension.provider) && !sameSignIn(signIns.get(extension.provider), extension.signIn)) {
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
signIns.set(extension.provider, extension.signIn);
|
|
364
|
+
for (const setting of extension.settings ?? []) {
|
|
365
|
+
if ((owners.get(setting) ?? extension.provider) !== extension.provider) return false;
|
|
366
|
+
owners.set(setting, extension.provider);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return true;
|
|
370
|
+
}
|
|
293
371
|
var FIRST_PARTY_ROLLOUT = {
|
|
294
372
|
refusal(set, running) {
|
|
373
|
+
if (!signInsAgree(set.extensions)) return "sign_ins_disagree";
|
|
295
374
|
if (Object.keys(set.extensions).some((name) => running[name] === void 0)) return "new_extension";
|
|
296
375
|
if (Object.keys(running).some((name) => set.extensions[name] === void 0)) return "removed_extension";
|
|
297
376
|
const moved = Object.entries(set.extensions).some(([name, ext]) => running[name]?.provider !== ext.provider);
|
|
298
|
-
|
|
377
|
+
if (moved) return "changes_provider";
|
|
378
|
+
const authorityMoved = Object.entries(set.extensions).some(
|
|
379
|
+
([name, ext]) => !sameSignIn(running[name]?.signIn, ext.signIn)
|
|
380
|
+
);
|
|
381
|
+
return authorityMoved ? "changes_sign_in_authority" : null;
|
|
299
382
|
}
|
|
300
383
|
};
|
|
301
384
|
function authorizedGrants(declared, authorized) {
|
|
@@ -332,5 +415,7 @@ export {
|
|
|
332
415
|
satisfiesCaret,
|
|
333
416
|
setArchiveKey,
|
|
334
417
|
setLockfileKey,
|
|
335
|
-
|
|
418
|
+
setRecordExtension,
|
|
419
|
+
setRecordKey,
|
|
420
|
+
signInsAgree
|
|
336
421
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@holokai/harbor-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0-rc.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The harbor extension to MCP Apps: the public contract a hosted view speaks, and a thin client over @modelcontextprotocol/ext-apps.",
|
|
6
6
|
"exports": {
|