@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 +16 -3
- package/dist/index.d.ts +6 -4
- package/dist/index.js +87 -3
- package/package.json +4 -3
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
|
-
{
|
|
53
|
-
|
|
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
|
|
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", "
|
|
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.
|
|
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
|
-
"
|
|
29
|
-
"@mdbase-dev/connect
|
|
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",
|