@mdbase-dev/connect-dev 0.1.0-beta.26 → 0.1.0-beta.28
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 +147 -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" };
|
|
@@ -29,6 +30,9 @@ export function validateAppManifest(value, options = {}) {
|
|
|
29
30
|
const originResult = validateManifestOrigins(value, options.allowLocal === true);
|
|
30
31
|
if (!originResult.valid)
|
|
31
32
|
return originResult;
|
|
33
|
+
const capabilityResult = validateCapabilityRequirements(value);
|
|
34
|
+
if (!capabilityResult.valid)
|
|
35
|
+
return capabilityResult;
|
|
32
36
|
return validateProvisionRequirements(value);
|
|
33
37
|
}
|
|
34
38
|
export function validateDataContract(value) {
|
|
@@ -60,6 +64,18 @@ export class DataContractDefinitionError extends Error {
|
|
|
60
64
|
* developers to calculate SHA-256 values by hand.
|
|
61
65
|
*/
|
|
62
66
|
export function defineTypePack(definition) {
|
|
67
|
+
const provides = definition.resources
|
|
68
|
+
.map((resource, index) => ({ resource, index }))
|
|
69
|
+
.filter(({ resource }) => resource.kind === "contract")
|
|
70
|
+
.map(({ resource, index }) => {
|
|
71
|
+
if (resource.mode !== "managed") {
|
|
72
|
+
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")]);
|
|
73
|
+
}
|
|
74
|
+
return exactContractReferenceFromDocument(resource.document, index);
|
|
75
|
+
})
|
|
76
|
+
.sort((left, right) => left.id.localeCompare(right.id)
|
|
77
|
+
|| left.version.localeCompare(right.version)
|
|
78
|
+
|| left.digest.localeCompare(right.digest));
|
|
63
79
|
const provision = {
|
|
64
80
|
manifest: {
|
|
65
81
|
kind: "mdbase.type-pack",
|
|
@@ -67,21 +83,75 @@ export function defineTypePack(definition) {
|
|
|
67
83
|
version: definition.version,
|
|
68
84
|
...(definition.name ? { name: definition.name } : {}),
|
|
69
85
|
...(definition.description ? { description: definition.description } : {}),
|
|
70
|
-
resources: definition.resources.map(({ kind, source, target = source, document }) => ({
|
|
86
|
+
resources: definition.resources.map(({ kind, mode, source, target = source, document }) => ({
|
|
71
87
|
kind,
|
|
88
|
+
mode,
|
|
72
89
|
source,
|
|
73
90
|
target,
|
|
74
91
|
digest: `sha256:${createHash("sha256").update(document).digest("hex")}`
|
|
75
92
|
}))
|
|
76
93
|
},
|
|
77
94
|
resources: definition.resources.map(({ source, document }) => ({ source, document })),
|
|
78
|
-
provides
|
|
95
|
+
provides
|
|
79
96
|
};
|
|
80
97
|
const result = validateProtocolValue(provision, "typePackProvision");
|
|
81
98
|
if (!result.valid)
|
|
82
99
|
throw new TypePackDefinitionError(result.issues);
|
|
83
100
|
return provision;
|
|
84
101
|
}
|
|
102
|
+
function exactContractReferenceFromDocument(document, resourceIndex) {
|
|
103
|
+
const frontmatter = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/u.exec(document)?.[1];
|
|
104
|
+
if (frontmatter === undefined) {
|
|
105
|
+
throw new TypePackDefinitionError([definitionIssue(`/resources/${resourceIndex}/document`, "contract_frontmatter", "contract resources must contain YAML frontmatter")]);
|
|
106
|
+
}
|
|
107
|
+
const parsed = parseDocument(frontmatter, { uniqueKeys: true });
|
|
108
|
+
if (parsed.errors.length > 0) {
|
|
109
|
+
throw new TypePackDefinitionError([definitionIssue(`/resources/${resourceIndex}/document`, "contract_frontmatter", `contract frontmatter is invalid: ${parsed.errors[0]?.message ?? "unknown YAML error"}`)]);
|
|
110
|
+
}
|
|
111
|
+
const contract = parsed.toJS({ maxAliasCount: 0 });
|
|
112
|
+
const validation = validateDataContract(contract);
|
|
113
|
+
if (!validation.valid)
|
|
114
|
+
throw new DataContractDefinitionError(validation.issues);
|
|
115
|
+
const value = contract;
|
|
116
|
+
return {
|
|
117
|
+
id: String(value.id),
|
|
118
|
+
version: String(value.version),
|
|
119
|
+
digest: dataContractDigest(value)
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
/** Compute the portable semantic digest of one validated, fully resolved contract. */
|
|
123
|
+
export function dataContractDigest(contract) {
|
|
124
|
+
const validation = validateDataContract(contract);
|
|
125
|
+
if (!validation.valid)
|
|
126
|
+
throw new DataContractDefinitionError(validation.issues);
|
|
127
|
+
const contractType = String(contract.contract_type);
|
|
128
|
+
const portable = {
|
|
129
|
+
kind: contract.kind,
|
|
130
|
+
contract_type: contract.contract_type,
|
|
131
|
+
id: contract.id,
|
|
132
|
+
version: contract.version
|
|
133
|
+
};
|
|
134
|
+
const schemaFields = contractType === "record"
|
|
135
|
+
? ["record_schema", "binding_schema"]
|
|
136
|
+
: contractType === "event"
|
|
137
|
+
? ["data_schema", "source_schema"]
|
|
138
|
+
: ["input_schema", "output_schema", "error_schema", "provider_schema"];
|
|
139
|
+
for (const field of schemaFields) {
|
|
140
|
+
const wrapper = contract[field];
|
|
141
|
+
if (wrapper === undefined)
|
|
142
|
+
continue;
|
|
143
|
+
if (!isRecord(wrapper) || !("value" in wrapper)) {
|
|
144
|
+
throw new DataContractDigestError(`${field} must be resolved to an inline JSON Schema before calculating a contract digest.`);
|
|
145
|
+
}
|
|
146
|
+
portable[field] = structuredClone(wrapper.value);
|
|
147
|
+
}
|
|
148
|
+
if (contractType === "action" && contract.behavior !== undefined) {
|
|
149
|
+
portable.behavior = structuredClone(contract.behavior);
|
|
150
|
+
}
|
|
151
|
+
return `sha256:${createHash("sha256").update(canonicalJson(portable)).digest("hex")}`;
|
|
152
|
+
}
|
|
153
|
+
export class DataContractDigestError extends Error {
|
|
154
|
+
}
|
|
85
155
|
export class TypePackDefinitionError extends Error {
|
|
86
156
|
issues;
|
|
87
157
|
constructor(issues) {
|
|
@@ -475,6 +545,63 @@ function validateProvisionRequirements(value) {
|
|
|
475
545
|
}
|
|
476
546
|
return { valid: true, issues: [] };
|
|
477
547
|
}
|
|
548
|
+
function validateCapabilityRequirements(value) {
|
|
549
|
+
const manifest = asObject(value);
|
|
550
|
+
const requirements = asObject(manifest.requirements);
|
|
551
|
+
const capabilities = asObject(requirements.capabilities);
|
|
552
|
+
if (Object.keys(capabilities).length === 0)
|
|
553
|
+
return { valid: true, issues: [] };
|
|
554
|
+
const required = Array.isArray(capabilities.required)
|
|
555
|
+
? capabilities.required.map(String)
|
|
556
|
+
: [];
|
|
557
|
+
const optional = Array.isArray(capabilities.optional)
|
|
558
|
+
? capabilities.optional.map(String)
|
|
559
|
+
: [];
|
|
560
|
+
if (new Set(required).size !== required.length) {
|
|
561
|
+
return semanticIssue("/requirements/capabilities/required", "must not contain duplicates");
|
|
562
|
+
}
|
|
563
|
+
if (new Set(optional).size !== optional.length) {
|
|
564
|
+
return semanticIssue("/requirements/capabilities/optional", "must not contain duplicates");
|
|
565
|
+
}
|
|
566
|
+
const overlap = optional.find((capability) => required.includes(capability));
|
|
567
|
+
if (overlap) {
|
|
568
|
+
return semanticIssue("/requirements/capabilities/optional", `must not repeat required capability ${overlap}`);
|
|
569
|
+
}
|
|
570
|
+
const declared = new Set([...required, ...optional]);
|
|
571
|
+
const provisions = asObject(manifest.provisions);
|
|
572
|
+
if (Array.isArray(requirements.contracts)
|
|
573
|
+
&& requirements.contracts.length > 0
|
|
574
|
+
&& requirements.access !== "full_collection"
|
|
575
|
+
&& !required.includes("definitions.contracts.current")) {
|
|
576
|
+
return semanticIssue("/requirements/capabilities/required", "must require definitions.contracts.current for contract-scoped requirements");
|
|
577
|
+
}
|
|
578
|
+
if (declared.has("definitions.type-pack.apply")) {
|
|
579
|
+
if (requirements.access !== "full_collection") {
|
|
580
|
+
return semanticIssue("/requirements/access", "must be full_collection for definitions.type-pack.apply");
|
|
581
|
+
}
|
|
582
|
+
if (!required.includes("definitions.type-pack.apply")) {
|
|
583
|
+
return semanticIssue("/requirements/capabilities/required", "must require definitions.type-pack.apply when it is declared");
|
|
584
|
+
}
|
|
585
|
+
if (!Array.isArray(provisions.type_packs) || provisions.type_packs.length === 0) {
|
|
586
|
+
return semanticIssue("/provisions/type_packs", "must contain a pack for definitions.type-pack.apply");
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
if (declared.has("notifications.background-delivery")
|
|
590
|
+
&& !Array.isArray(asObject(manifest.notifications).criteria)) {
|
|
591
|
+
return semanticIssue("/notifications/criteria", "must be declared for notifications.background-delivery");
|
|
592
|
+
}
|
|
593
|
+
const fileRequirement = asObject(requirements.files);
|
|
594
|
+
const fileActions = Array.isArray(fileRequirement.actions)
|
|
595
|
+
? new Set(fileRequirement.actions.map(String))
|
|
596
|
+
: new Set();
|
|
597
|
+
for (const action of ["list", "read", "add", "replace", "move", "delete"]) {
|
|
598
|
+
const capability = `files.${action}`;
|
|
599
|
+
if (declared.has(capability) !== fileActions.has(action)) {
|
|
600
|
+
return semanticIssue("/requirements/capabilities", `${capability} and requirements.files.actions.${action} must be declared together`);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
return { valid: true, issues: [] };
|
|
604
|
+
}
|
|
478
605
|
function localManifestSchemaCandidate(value) {
|
|
479
606
|
const candidate = clone(value);
|
|
480
607
|
const object = asObject(candidate);
|
|
@@ -511,13 +638,30 @@ function semanticIssue(path, message) {
|
|
|
511
638
|
issues: [{ path, keyword: "semantic", message, params: {} }]
|
|
512
639
|
};
|
|
513
640
|
}
|
|
641
|
+
function definitionIssue(path, keyword, message) {
|
|
642
|
+
return { path, keyword, message, params: {} };
|
|
643
|
+
}
|
|
644
|
+
function isRecord(value) {
|
|
645
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
646
|
+
}
|
|
647
|
+
function canonicalJson(value) {
|
|
648
|
+
if (value === null || typeof value !== "object")
|
|
649
|
+
return JSON.stringify(value);
|
|
650
|
+
if (Array.isArray(value))
|
|
651
|
+
return `[${value.map(canonicalJson).join(",")}]`;
|
|
652
|
+
return `{${Object.entries(value)
|
|
653
|
+
.filter(([, child]) => child !== undefined)
|
|
654
|
+
.sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0)
|
|
655
|
+
.map(([key, child]) => `${JSON.stringify(key)}:${canonicalJson(child)}`)
|
|
656
|
+
.join(",")}}`;
|
|
657
|
+
}
|
|
514
658
|
function sandboxDescription(value = {}) {
|
|
515
659
|
return {
|
|
516
660
|
protocol_version: 1,
|
|
517
661
|
collection_id: value.collection_id ?? "01900000-0000-7000-8000-000000000001",
|
|
518
662
|
display_name: value.display_name ?? "Developer sandbox",
|
|
519
663
|
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", "
|
|
664
|
+
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", "assess_type_pack", "apply_type_pack"],
|
|
521
665
|
change_cursor: value.change_cursor ?? 0,
|
|
522
666
|
types: clone(value.types ?? []),
|
|
523
667
|
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.28",
|
|
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.28",
|
|
30
|
+
"@mdbase-dev/connect-protocol": "0.1.0-beta.28"
|
|
30
31
|
},
|
|
31
32
|
"devDependencies": {
|
|
32
33
|
"@types/node": "^24.0.0",
|