@mdbase-dev/connect-protocol 0.1.0-beta.124 → 0.1.0-beta.125

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.
@@ -60,3 +60,10 @@ export declare function parseAppManifest(value: unknown, options?: ManifestValid
60
60
  /** Validate and clone without changing capability IDs, file aliases, or version. */
61
61
  export declare function parseVersionedAppManifest(value: unknown, options?: ManifestValidationOptions): VersionedAppManifest;
62
62
  export declare function formatManifestValidationIssues(issues: readonly ManifestValidationIssue[]): string;
63
+ /**
64
+ * Validate one schema-valid type-pack provision beyond its JSON Schema: the
65
+ * embedded resources are exactly the manifest's, each digest pins its bytes,
66
+ * and every seed `upgrade_from` baseline satisfies mdbase 05A. Issue paths are
67
+ * prefixed with `path`, the provision's location in its enclosing document.
68
+ */
69
+ export declare function validateTypePackProvision(value: unknown, path?: string): ManifestValidationIssue[];
package/dist/manifest.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { Ajv2020 } from "ajv/dist/2020.js";
3
3
  import addFormatsModule from "ajv-formats";
4
+ import { isMap, parseDocument } from "yaml";
4
5
  import appManifestSchema from "../schemas/mdbase-app.schema.json" with { type: "json" };
5
6
  import legacyAppManifestSchema from "../schemas/mdbase-app.legacy-v1.schema.json" with { type: "json" };
6
7
  import { RECORD_EXTENSIONS_CONFIGURATION_PATH } from "./collection-setup.js";
@@ -266,47 +267,99 @@ function validateProvisionRequirements(value) {
266
267
  issues.push(issue(`/provisions/type_packs/${packIndex}/provides`, "contractRequirement", "may only contain contracts required by the application"));
267
268
  }
268
269
  }
269
- const packManifest = asObject(provision.manifest);
270
- const declaredResources = Array.isArray(packManifest.resources)
271
- ? packManifest.resources.map(asObject)
272
- : [];
273
- const embeddedResources = Array.isArray(provision.resources)
274
- ? provision.resources.map(asObject)
275
- : [];
276
- const embedded = new Map(embeddedResources.map((resource) => [String(resource.source), resource.document]));
277
- if (embedded.size !== embeddedResources.length
278
- || new Set(declaredResources.map((resource) => String(resource.source))).size
279
- !== declaredResources.length
280
- || declaredResources.length !== embeddedResources.length) {
281
- issues.push(issue(`/provisions/type_packs/${packIndex}/resources`, "resourceSet", "must match manifest source paths exactly"));
270
+ issues.push(...validateTypePackProvision(provision, `/provisions/type_packs/${packIndex}`));
271
+ }
272
+ return issues;
273
+ }
274
+ /**
275
+ * Validate one schema-valid type-pack provision beyond its JSON Schema: the
276
+ * embedded resources are exactly the manifest's, each digest pins its bytes,
277
+ * and every seed `upgrade_from` baseline satisfies mdbase 05A. Issue paths are
278
+ * prefixed with `path`, the provision's location in its enclosing document.
279
+ */
280
+ export function validateTypePackProvision(value, path = "") {
281
+ const provision = asObject(value);
282
+ const manifest = asObject(provision.manifest);
283
+ const declaredResources = Array.isArray(manifest.resources)
284
+ ? manifest.resources.map(asObject)
285
+ : [];
286
+ const embeddedResources = Array.isArray(provision.resources)
287
+ ? provision.resources.map(asObject)
288
+ : [];
289
+ const embedded = new Map(embeddedResources.map((resource) => [String(resource.source), resource.document]));
290
+ if (embedded.size !== embeddedResources.length
291
+ || new Set(declaredResources.map((resource) => String(resource.source))).size
292
+ !== declaredResources.length
293
+ || declaredResources.length !== embeddedResources.length) {
294
+ return [issue(`${path}/resources`, "resourceSet", "must match manifest source paths exactly")];
295
+ }
296
+ const issues = [];
297
+ for (const [resourceIndex, resource] of declaredResources.entries()) {
298
+ const resourcePath = `${path}/manifest/resources/${resourceIndex}`;
299
+ const source = String(resource.source);
300
+ const document = embedded.get(source);
301
+ if (typeof document !== "string") {
302
+ issues.push(issue(`${path}/resources`, "resourceSet", `is missing manifest source ${source}`));
282
303
  continue;
283
304
  }
284
- for (const [resourceIndex, resource] of declaredResources.entries()) {
285
- const source = String(resource.source);
286
- if (resource.upgrade_from !== undefined) {
287
- const baseline = asObject(resource.upgrade_from);
288
- const path = `/provisions/type_packs/${packIndex}/manifest/resources/${resourceIndex}/upgrade_from`;
289
- if (resource.kind !== "type" || resource.mode !== "seed") {
290
- issues.push(issue(path, "seedUpgrade", "is only allowed for seed types"));
291
- }
292
- if (typeof baseline.document === "string"
293
- && baseline.digest !== `sha256:${createHash("sha256").update(baseline.document).digest("hex")}`) {
294
- issues.push(issue(`${path}/digest`, "digest", "does not match the baseline document"));
295
- }
296
- }
297
- const document = embedded.get(source);
298
- if (typeof document !== "string") {
299
- issues.push(issue(`/provisions/type_packs/${packIndex}/resources`, "resourceSet", `is missing manifest source ${source}`));
300
- continue;
301
- }
302
- const digest = `sha256:${createHash("sha256").update(document).digest("hex")}`;
303
- if (digest !== resource.digest) {
304
- issues.push(issue(`/provisions/type_packs/${packIndex}/manifest/resources/${resourceIndex}/digest`, "digest", "does not match the embedded document"));
305
- }
305
+ if (sha256(document) !== resource.digest) {
306
+ issues.push(issue(`${resourcePath}/digest`, "digest", "does not match the embedded document"));
307
+ }
308
+ if (resource.upgrade_from !== undefined) {
309
+ issues.push(...validateSeedUpgrade(resource, document, `${resourcePath}/upgrade_from`));
310
+ }
311
+ }
312
+ return issues;
313
+ }
314
+ /** 05A seed-upgrade baselines: exact, distinct, earlier starters of the same type. */
315
+ function validateSeedUpgrade(resource, document, path) {
316
+ if (resource.kind !== "type" || resource.mode !== "seed") {
317
+ return [issue(path, "seedUpgrade", "is only allowed for seed types")];
318
+ }
319
+ const desired = typeFrontmatter(document);
320
+ const listed = Array.isArray(resource.upgrade_from);
321
+ const baselines = (listed ? resource.upgrade_from : [resource.upgrade_from]).map(asObject);
322
+ const seen = new Set();
323
+ const issues = [];
324
+ for (const [index, baseline] of baselines.entries()) {
325
+ const baselinePath = listed ? `${path}/${index}` : path;
326
+ const baselineDocument = String(baseline.document);
327
+ if (baseline.digest !== sha256(baselineDocument)) {
328
+ issues.push(issue(`${baselinePath}/digest`, "digest", "does not match the baseline document"));
329
+ }
330
+ else if (seen.has(baseline.digest)) {
331
+ issues.push(issue(`${baselinePath}/digest`, "uniqueBaseline", "duplicates another upgrade baseline"));
332
+ }
333
+ else if (baseline.digest === resource.digest) {
334
+ issues.push(issue(`${baselinePath}/digest`, "seedUpgrade", "must not be the resource's own document"));
335
+ }
336
+ seen.add(baseline.digest);
337
+ const previous = typeFrontmatter(baselineDocument);
338
+ if (!desired || !previous || previous.kind !== desired.kind || previous.name !== desired.name) {
339
+ issues.push(issue(`${baselinePath}/document`, "seedUpgrade", "must be a type document with the same kind and name as the resource"));
340
+ }
341
+ else if (baseline.version !== undefined && previous.version !== baseline.version) {
342
+ issues.push(issue(`${baselinePath}/version`, "seedUpgrade", "must equal the version the baseline document declares"));
306
343
  }
307
344
  }
308
345
  return issues;
309
346
  }
347
+ /** Top-level frontmatter of a type document, or undefined when it has none. */
348
+ // The identity a type document declares. Only these top-level keys are read, so
349
+ // anchors and aliases elsewhere in a starter are harmless and alias expansion
350
+ // stays disabled.
351
+ function typeFrontmatter(document) {
352
+ const source = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/u.exec(document)?.[1];
353
+ if (source === undefined)
354
+ return undefined;
355
+ const parsed = parseDocument(source, { uniqueKeys: true });
356
+ if (parsed.errors.length > 0 || !isMap(parsed.contents))
357
+ return undefined;
358
+ return { kind: parsed.get("kind"), name: parsed.get("name"), version: parsed.get("version") };
359
+ }
360
+ function sha256(document) {
361
+ return `sha256:${createHash("sha256").update(document).digest("hex")}`;
362
+ }
310
363
  function validateConfigurationSetup(value) {
311
364
  const manifest = asObject(value);
312
365
  const requirements = asObject(manifest.requirements);
@@ -35,12 +35,19 @@ export declare const MDBASE_TIMER_FIRED_CONTRACT: {
35
35
  readonly version: "1.0.0";
36
36
  readonly digest: "sha256:41105be7a7abf33b31ced47e1e1965242236e40ccaea286b959b0a8c591f5642";
37
37
  };
38
+ /** Exact bytes of a starter the publisher previously shipped for a seed type. */
39
+ type TypePackUpgradeBaseline = {
40
+ digest: string;
41
+ document: string;
42
+ /** The `version` the baseline document declares, for presentation only. */
43
+ version?: number;
44
+ };
38
45
  export interface TypePackManifestResource {
39
- /** Explicit three-way upgrade; only valid for seed type resources. */
40
- upgrade_from?: {
41
- digest: string;
42
- document: string;
43
- };
46
+ /**
47
+ * Previously shipped starters this seed type may be upgraded from; only valid
48
+ * for seed type resources. A single baseline is equivalent to a one-item list.
49
+ */
50
+ upgrade_from?: TypePackUpgradeBaseline | TypePackUpgradeBaseline[];
44
51
  kind: "contract" | "type" | "schema";
45
52
  mode: "managed" | "seed";
46
53
  source: string;
@@ -78,6 +85,11 @@ export interface TypePackResourceDiff {
78
85
  current_digest?: string;
79
86
  installed_digest?: string;
80
87
  adopted_from_digest?: string;
88
+ /** The declared baseline an engine upgraded this seed from (seed `update` only). */
89
+ upgrade_baseline?: {
90
+ digest: string;
91
+ version?: number;
92
+ };
81
93
  reason?: string;
82
94
  }
83
95
  export interface TypePackReceipt {
@@ -85,7 +97,10 @@ export interface TypePackReceipt {
85
97
  version: string;
86
98
  digest: string;
87
99
  installed_by: string;
88
- resources: Omit<TypePackResourceDiff, "action" | "current_digest" | "installed_digest" | "reason">[];
100
+ resources: Array<Omit<TypePackResourceDiff, "action" | "current_digest" | "installed_digest" | "upgrade_baseline" | "reason"> & {
101
+ /** Seed only: the publisher document the target descends from, when known. */
102
+ origin_digest?: string;
103
+ }>;
89
104
  }
90
105
  export interface TypePackAssessment {
91
106
  status: "current" | "install" | "upgrade" | "downgrade" | "reconfigure" | "conflict";
@@ -136,3 +151,4 @@ export type ContractSetupChoice = {
136
151
  fields: Record<string, string>;
137
152
  binding?: Record<string, unknown>;
138
153
  };
154
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mdbase-dev/connect-protocol",
3
- "version": "0.1.0-beta.124",
3
+ "version": "0.1.0-beta.125",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -28,7 +28,8 @@
28
28
  ],
29
29
  "dependencies": {
30
30
  "ajv": "^8.20.0",
31
- "ajv-formats": "^3.0.1"
31
+ "ajv-formats": "^3.0.1",
32
+ "yaml": "^2.9.1"
32
33
  },
33
34
  "devDependencies": {
34
35
  "@types/node": "^24.0.0",
@@ -410,11 +410,27 @@
410
410
  "properties": {
411
411
  "kind": { "enum": ["contract", "type", "schema"] },
412
412
  "mode": { "enum": ["managed", "seed"] },
413
+ "upgrade_from": {
414
+ "oneOf": [
415
+ { "$ref": "#/$defs/typePackUpgradeBaseline" },
416
+ { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/typePackUpgradeBaseline" } }
417
+ ]
418
+ },
413
419
  "source": { "type": "string", "minLength": 1, "maxLength": 500 },
414
420
  "target": { "type": "string", "minLength": 1, "maxLength": 500 },
415
421
  "digest": { "$ref": "#/$defs/digest" }
416
422
  }
417
423
  },
424
+ "typePackUpgradeBaseline": {
425
+ "type": "object",
426
+ "additionalProperties": false,
427
+ "required": ["digest", "document"],
428
+ "properties": {
429
+ "digest": { "$ref": "#/$defs/digest" },
430
+ "document": { "type": "string", "maxLength": 262144 },
431
+ "version": { "type": "integer", "minimum": 1 }
432
+ }
433
+ },
418
434
  "typePackSourceResource": {
419
435
  "type": "object",
420
436
  "additionalProperties": false,
@@ -319,19 +319,26 @@
319
319
  "kind": { "enum": ["contract", "type", "schema"] },
320
320
  "mode": { "enum": ["managed", "seed"] },
321
321
  "upgrade_from": {
322
- "type": "object",
323
- "additionalProperties": false,
324
- "required": ["digest", "document"],
325
- "properties": {
326
- "digest": { "type": "string", "pattern": "^sha256:[0-9a-f]{64}$" },
327
- "document": { "type": "string", "maxLength": 262144 }
328
- }
322
+ "oneOf": [
323
+ { "$ref": "#/$defs/typePackUpgradeBaseline" },
324
+ { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/typePackUpgradeBaseline" } }
325
+ ]
329
326
  },
330
327
  "source": { "type": "string", "minLength": 1, "maxLength": 240 },
331
328
  "target": { "type": "string", "minLength": 1, "maxLength": 240 },
332
329
  "digest": { "type": "string", "pattern": "^sha256:[0-9a-f]{64}$" }
333
330
  }
334
331
  },
332
+ "typePackUpgradeBaseline": {
333
+ "type": "object",
334
+ "additionalProperties": false,
335
+ "required": ["digest", "document"],
336
+ "properties": {
337
+ "digest": { "type": "string", "pattern": "^sha256:[0-9a-f]{64}$" },
338
+ "document": { "type": "string", "maxLength": 262144 },
339
+ "version": { "type": "integer", "minimum": 1 }
340
+ }
341
+ },
335
342
  "typePackSourceResource": {
336
343
  "type": "object",
337
344
  "additionalProperties": false,