@mdbase-dev/connect-dev 0.1.0-beta.25 → 0.1.0-beta.27

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/README.md CHANGED
@@ -47,14 +47,27 @@ import { defineTypePack } from "@mdbase-dev/connect-dev";
47
47
  const provision = defineTypePack({
48
48
  id: "example.tasks",
49
49
  version: "1.0.0",
50
- provides: [{ id: "example.task", version: "1.0.0" }],
51
50
  resources: [
52
- { kind: "contract", source: "_contracts/example.task.md", document: contractDocument },
53
- { kind: "type", source: "_types/task.md", document: typeDocument }
51
+ {
52
+ kind: "contract",
53
+ mode: "managed",
54
+ source: "_contracts/example.task.md",
55
+ document: contractDocument
56
+ },
57
+ {
58
+ kind: "type",
59
+ mode: "seed",
60
+ source: "_types/task.md",
61
+ document: typeDocument
62
+ }
54
63
  ]
55
64
  });
56
65
  ```
57
66
 
67
+ The helper validates each contract document and derives the exact semantic
68
+ `provides` descriptors. Managed resources evolve with the pack; seed resources
69
+ become collection-owned after their initial creation.
70
+
58
71
  Native applications may add a reverse-domain private-use callback scheme that
59
72
  matches the v1 manifest's application ID, such as
60
73
  `example.tasks.desktop://auth/mdbase/callback` for
package/dist/index.d.ts CHANGED
@@ -45,6 +45,8 @@ export declare class DataContractDefinitionError extends Error {
45
45
  }
46
46
  export interface TypePackResourceInput {
47
47
  kind: "contract" | "type" | "schema";
48
+ /** Managed resources evolve with the pack; seed resources become user-owned after creation. */
49
+ mode: "managed" | "seed";
48
50
  source: string;
49
51
  /** Collection-relative install location. Defaults to `source`. */
50
52
  target?: string;
@@ -57,10 +59,6 @@ export interface TypePackDefinition {
57
59
  name?: string;
58
60
  description?: string;
59
61
  resources: TypePackResourceInput[];
60
- provides: Array<{
61
- id: string;
62
- version: string;
63
- }>;
64
62
  }
65
63
  /**
66
64
  * Build a complete, digest-pinned type-pack provision from readable source
@@ -68,6 +66,10 @@ export interface TypePackDefinition {
68
66
  * developers to calculate SHA-256 values by hand.
69
67
  */
70
68
  export declare function defineTypePack(definition: TypePackDefinition): TypePackProvision;
69
+ /** Compute the portable semantic digest of one validated, fully resolved contract. */
70
+ export declare function dataContractDigest(contract: Record<string, unknown>): string;
71
+ export declare class DataContractDigestError extends Error {
72
+ }
71
73
  export declare class TypePackDefinitionError extends Error {
72
74
  readonly issues: ValidationIssue[];
73
75
  constructor(issues: ValidationIssue[]);
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { Ajv2020 } from "ajv/dist/2020.js";
3
+ import { parseDocument } from "yaml";
3
4
  import { MdbaseCollectionClient, connectError } from "@mdbase-dev/connect";
4
5
  import { isNativeRedirectUri } from "@mdbase-dev/connect-protocol";
5
6
  import appManifestSchema from "@mdbase-dev/connect-protocol/schemas/mdbase-app.schema.json" with { type: "json" };
@@ -60,6 +61,18 @@ export class DataContractDefinitionError extends Error {
60
61
  * developers to calculate SHA-256 values by hand.
61
62
  */
62
63
  export function defineTypePack(definition) {
64
+ const provides = definition.resources
65
+ .map((resource, index) => ({ resource, index }))
66
+ .filter(({ resource }) => resource.kind === "contract")
67
+ .map(({ resource, index }) => {
68
+ if (resource.mode !== "managed") {
69
+ throw new TypePackDefinitionError([definitionIssue(`/resources/${index}/mode`, "contract_resource_mode", "contract resources must be managed so their declared version and digest can evolve with the pack")]);
70
+ }
71
+ return exactContractReferenceFromDocument(resource.document, index);
72
+ })
73
+ .sort((left, right) => left.id.localeCompare(right.id)
74
+ || left.version.localeCompare(right.version)
75
+ || left.digest.localeCompare(right.digest));
63
76
  const provision = {
64
77
  manifest: {
65
78
  kind: "mdbase.type-pack",
@@ -67,21 +80,75 @@ export function defineTypePack(definition) {
67
80
  version: definition.version,
68
81
  ...(definition.name ? { name: definition.name } : {}),
69
82
  ...(definition.description ? { description: definition.description } : {}),
70
- resources: definition.resources.map(({ kind, source, target = source, document }) => ({
83
+ resources: definition.resources.map(({ kind, mode, source, target = source, document }) => ({
71
84
  kind,
85
+ mode,
72
86
  source,
73
87
  target,
74
88
  digest: `sha256:${createHash("sha256").update(document).digest("hex")}`
75
89
  }))
76
90
  },
77
91
  resources: definition.resources.map(({ source, document }) => ({ source, document })),
78
- provides: definition.provides.map((contract) => ({ ...contract }))
92
+ provides
79
93
  };
80
94
  const result = validateProtocolValue(provision, "typePackProvision");
81
95
  if (!result.valid)
82
96
  throw new TypePackDefinitionError(result.issues);
83
97
  return provision;
84
98
  }
99
+ function exactContractReferenceFromDocument(document, resourceIndex) {
100
+ const frontmatter = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/u.exec(document)?.[1];
101
+ if (frontmatter === undefined) {
102
+ throw new TypePackDefinitionError([definitionIssue(`/resources/${resourceIndex}/document`, "contract_frontmatter", "contract resources must contain YAML frontmatter")]);
103
+ }
104
+ const parsed = parseDocument(frontmatter, { uniqueKeys: true });
105
+ if (parsed.errors.length > 0) {
106
+ throw new TypePackDefinitionError([definitionIssue(`/resources/${resourceIndex}/document`, "contract_frontmatter", `contract frontmatter is invalid: ${parsed.errors[0]?.message ?? "unknown YAML error"}`)]);
107
+ }
108
+ const contract = parsed.toJS({ maxAliasCount: 0 });
109
+ const validation = validateDataContract(contract);
110
+ if (!validation.valid)
111
+ throw new DataContractDefinitionError(validation.issues);
112
+ const value = contract;
113
+ return {
114
+ id: String(value.id),
115
+ version: String(value.version),
116
+ digest: dataContractDigest(value)
117
+ };
118
+ }
119
+ /** Compute the portable semantic digest of one validated, fully resolved contract. */
120
+ export function dataContractDigest(contract) {
121
+ const validation = validateDataContract(contract);
122
+ if (!validation.valid)
123
+ throw new DataContractDefinitionError(validation.issues);
124
+ const contractType = String(contract.contract_type);
125
+ const portable = {
126
+ kind: contract.kind,
127
+ contract_type: contract.contract_type,
128
+ id: contract.id,
129
+ version: contract.version
130
+ };
131
+ const schemaFields = contractType === "record"
132
+ ? ["record_schema", "binding_schema"]
133
+ : contractType === "event"
134
+ ? ["data_schema", "source_schema"]
135
+ : ["input_schema", "output_schema", "error_schema", "provider_schema"];
136
+ for (const field of schemaFields) {
137
+ const wrapper = contract[field];
138
+ if (wrapper === undefined)
139
+ continue;
140
+ if (!isRecord(wrapper) || !("value" in wrapper)) {
141
+ throw new DataContractDigestError(`${field} must be resolved to an inline JSON Schema before calculating a contract digest.`);
142
+ }
143
+ portable[field] = structuredClone(wrapper.value);
144
+ }
145
+ if (contractType === "action" && contract.behavior !== undefined) {
146
+ portable.behavior = structuredClone(contract.behavior);
147
+ }
148
+ return `sha256:${createHash("sha256").update(canonicalJson(portable)).digest("hex")}`;
149
+ }
150
+ export class DataContractDigestError extends Error {
151
+ }
85
152
  export class TypePackDefinitionError extends Error {
86
153
  issues;
87
154
  constructor(issues) {
@@ -511,13 +578,30 @@ function semanticIssue(path, message) {
511
578
  issues: [{ path, keyword: "semantic", message, params: {} }]
512
579
  };
513
580
  }
581
+ function definitionIssue(path, keyword, message) {
582
+ return { path, keyword, message, params: {} };
583
+ }
584
+ function isRecord(value) {
585
+ return value !== null && typeof value === "object" && !Array.isArray(value);
586
+ }
587
+ function canonicalJson(value) {
588
+ if (value === null || typeof value !== "object")
589
+ return JSON.stringify(value);
590
+ if (Array.isArray(value))
591
+ return `[${value.map(canonicalJson).join(",")}]`;
592
+ return `{${Object.entries(value)
593
+ .filter(([, child]) => child !== undefined)
594
+ .sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0)
595
+ .map(([key, child]) => `${JSON.stringify(key)}:${canonicalJson(child)}`)
596
+ .join(",")}}`;
597
+ }
514
598
  function sandboxDescription(value = {}) {
515
599
  return {
516
600
  protocol_version: 1,
517
601
  collection_id: value.collection_id ?? "01900000-0000-7000-8000-000000000001",
518
602
  display_name: value.display_name ?? "Developer sandbox",
519
603
  spec_version: value.spec_version ?? "0.3.0",
520
- operations: value.operations ?? ["describe", "changes", "read", "query", "list_views", "execute_view", "read_view_source", "validate", "create", "update", "delete", "rename", "create_view_source", "update_view_source", "delete_view_source", "read_type", "create_type", "update_type", "install_type_pack"],
604
+ operations: value.operations ?? ["describe", "changes", "read", "query", "list_views", "execute_view", "read_view_source", "validate", "create", "update", "delete", "rename", "create_view_source", "update_view_source", "delete_view_source", "read_type", "create_type", "update_type", "apply_type_pack"],
521
605
  change_cursor: value.change_cursor ?? 0,
522
606
  types: clone(value.types ?? []),
523
607
  contracts: clone(value.contracts ?? [])
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mdbase-dev/connect-dev",
3
- "version": "0.1.0-beta.25",
3
+ "version": "0.1.0-beta.27",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -25,8 +25,9 @@
25
25
  ],
26
26
  "dependencies": {
27
27
  "ajv": "^8.20.0",
28
- "@mdbase-dev/connect": "0.1.0-beta.25",
29
- "@mdbase-dev/connect-protocol": "0.1.0-beta.25"
28
+ "yaml": "^2.9.0",
29
+ "@mdbase-dev/connect": "0.1.0-beta.27",
30
+ "@mdbase-dev/connect-protocol": "0.1.0-beta.27"
30
31
  },
31
32
  "devDependencies": {
32
33
  "@types/node": "^24.0.0",