@holokai/harbor-sdk 0.5.0-rc.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "package": "@holokai/harbor-sdk",
3
- "version": "0.5.0-rc.0",
4
- "digest": "cc9eed9b0678ea2d2300006bdf45b0dbf1dec99eff125a9c7d22133db1f99a9e",
5
- "commit": "acdb1a6f7bb8a8e52334cc4e7f45729a406bb09e"
3
+ "version": "0.6.0-rc.0",
4
+ "digest": "ee14f279ce9678445cfa96aca7c3c14e5a2ed36ad5f7ba0586d49f3a61aa36a8",
5
+ "commit": "86e86d6f6f6131ff2d8bc9117e165d3f9000f8a2"
6
6
  }
@@ -1,4 +1,4 @@
1
- /** The manifest type a first-party extension exports and harbor reads. */
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
- /** One extension a release set holds. */
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
- * The first-party default: a set that adds an extension, drops one the running tree holds, or moves one to another
232
- * provider waits for an app release.
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
- return { version, provider, grants: [...grants] };
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
- return moved ? "changes_provider" : null;
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
- setRecordKey
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.5.0-rc.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": {