@mdbase-dev/connect-protocol 0.1.0-beta.100

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.
Files changed (51) hide show
  1. package/LICENSE +21 -0
  2. package/dist/application-authorization.d.ts +32 -0
  3. package/dist/application-authorization.js +207 -0
  4. package/dist/capabilities.d.ts +73 -0
  5. package/dist/capabilities.js +92 -0
  6. package/dist/collection-setup.d.ts +90 -0
  7. package/dist/collection-setup.js +1 -0
  8. package/dist/compatibility.d.ts +37 -0
  9. package/dist/compatibility.js +65 -0
  10. package/dist/connect-problems.generated.d.ts +1388 -0
  11. package/dist/connect-problems.generated.js +574 -0
  12. package/dist/files.d.ts +269 -0
  13. package/dist/files.js +426 -0
  14. package/dist/index.d.ts +878 -0
  15. package/dist/index.js +94 -0
  16. package/dist/manifest.d.ts +62 -0
  17. package/dist/manifest.js +527 -0
  18. package/dist/mutation-fingerprint.d.ts +6 -0
  19. package/dist/mutation-fingerprint.js +116 -0
  20. package/dist/operations.d.ts +15 -0
  21. package/dist/operations.js +194 -0
  22. package/dist/type-packs.d.ts +133 -0
  23. package/dist/type-packs.js +30 -0
  24. package/package.json +42 -0
  25. package/schemas/application-capability-catalog.v1.json +40 -0
  26. package/schemas/application-capability-catalog.v2.json +68 -0
  27. package/schemas/catalog.json +17 -0
  28. package/schemas/connect-problem.v1.schema.json +5132 -0
  29. package/schemas/connect-problems.v1.catalog.json +351 -0
  30. package/schemas/connect-protocol.v1.schema.json +800 -0
  31. package/schemas/data-contract.schema.json +163 -0
  32. package/schemas/encrypted-relay.v1.schema.json +63 -0
  33. package/schemas/encrypted-relay.v2.schema.json +63 -0
  34. package/schemas/encrypted-relay.v3.schema.json +64 -0
  35. package/schemas/files.v1.schema.json +507 -0
  36. package/schemas/interop/v0.1/action-cancellation.schema.json +6 -0
  37. package/schemas/interop/v0.1/action-invocation.schema.json +6 -0
  38. package/schemas/interop/v0.1/action-outcome.schema.json +6 -0
  39. package/schemas/interop/v0.1/action-provider-declaration.schema.json +6 -0
  40. package/schemas/interop/v0.1/action-request.schema.json +6 -0
  41. package/schemas/interop/v0.1/conformance-claim.schema.json +6 -0
  42. package/schemas/interop/v0.1/event-source-declaration.schema.json +6 -0
  43. package/schemas/interop/v0.1/event.schema.json +6 -0
  44. package/schemas/interop/v0.1/profile.schema.json +456 -0
  45. package/schemas/mdbase-app.legacy-v1.schema.json +405 -0
  46. package/schemas/mdbase-app.schema.json +381 -0
  47. package/schemas/mdbase-query.v0.3.schema.json +171 -0
  48. package/schemas/notification-webhook.v1.schema.json +56 -0
  49. package/schemas/operation-catalog.v1.json +42 -0
  50. package/schemas/operation-mutation-classification.v1.json +537 -0
  51. package/schemas/sync.v1.schema.json +483 -0
package/dist/index.js ADDED
@@ -0,0 +1,94 @@
1
+ import { FRESH_APPLICATION_AUTHORIZATION_CAPABILITIES } from "./capabilities.js";
2
+ export * from "./connect-problems.generated.js";
3
+ export * from "./files.js";
4
+ export * from "./operations.js";
5
+ export * from "./mutation-fingerprint.js";
6
+ export * from "./compatibility.js";
7
+ export * from "./capabilities.js";
8
+ export * from "./application-authorization.js";
9
+ export * from "./type-packs.js";
10
+ export * from "./collection-setup.js";
11
+ export const CONTROL_PROTOCOL_VERSION = 1;
12
+ export { AUTHORIZATION_BINDING_PROTOCOL_VERSION as APPLICATION_AUTHORIZATION_PROTOCOL_VERSION } from "./compatibility.js";
13
+ export const LOOPBACK_PROTOCOL_VERSION = 1;
14
+ export const DEFAULT_LOOPBACK_PORT = 28_485;
15
+ export const RELAY_ENCRYPTION_SUITE = "P256-HKDF-SHA256-AES256GCM";
16
+ export const GRANT_ENCRYPTION_PROTOCOL_VERSION = 1;
17
+ export const SYNC_PROTOCOL_VERSION = 1;
18
+ export const CONTRACT_SETUP_CAPABILITY = "contract-setup-v1";
19
+ export const FILE_RELAY_CAPABILITY = "file-relay-v1";
20
+ export const PROTOCOL_USAGE_REPORT_CAPABILITY = "protocol-usage-report-v1";
21
+ export const POLICY_FRESHNESS_LEASE_CAPABILITY = "policy-freshness-lease-v1";
22
+ /** Advertised beta capabilities are intentionally broader than the beta baseline. */
23
+ export const RELAY_REQUIRED_CAPABILITIES = [
24
+ "application-authorization-v4",
25
+ "authorization-activation",
26
+ "encrypted-relay",
27
+ "policy-ack"
28
+ ];
29
+ export const MINIMUM_CONNECTOR_VERSION = "0.1.0-beta.33";
30
+ export const POLICY_FRESHNESS_LEASE_MINIMUM_CONNECTOR_VERSION = "0.1.0-beta.91";
31
+ export const HOSTED_PROVIDER_REQUIRED_CAPABILITIES = [
32
+ "durable-mutation-journal-v1",
33
+ "durable-file-lifecycle-v1"
34
+ ];
35
+ export const HOSTED_CANDIDATE_B_ACTIVATION_CAPABILITY = "candidate-b-activation-v1";
36
+ export const HOSTED_PROVIDER_CAPABILITIES = [
37
+ ...FRESH_APPLICATION_AUTHORIZATION_CAPABILITIES,
38
+ ...HOSTED_PROVIDER_REQUIRED_CAPABILITIES,
39
+ "mutation-replay-after-credential-retirement-v1",
40
+ HOSTED_CANDIDATE_B_ACTIVATION_CAPABILITY
41
+ ];
42
+ export const APPLICATION_DECLARATION_EVIDENCE_CAPABILITY = "application-declaration-evidence-v1";
43
+ export const RELAY_CAPABILITIES = [
44
+ ...FRESH_APPLICATION_AUTHORIZATION_CAPABILITIES,
45
+ APPLICATION_DECLARATION_EVIDENCE_CAPABILITY,
46
+ ...RELAY_REQUIRED_CAPABILITIES,
47
+ POLICY_FRESHNESS_LEASE_CAPABILITY,
48
+ "application-authorization-v5",
49
+ CONTRACT_SETUP_CAPABILITY,
50
+ FILE_RELAY_CAPABILITY,
51
+ PROTOCOL_USAGE_REPORT_CAPABILITY
52
+ ];
53
+ export const AUTHORITY_PROOF_VERSION = 1;
54
+ export const AUTHORITY_PROOF_ALGORITHM = "P256-SHA256";
55
+ export const AUTHORITY_PROOF_DOMAIN = "mdbase-authority-request-proof-v1";
56
+ export const AUTHORITY_PROOF_HEADERS = {
57
+ version: "x-mdbase-proof-version",
58
+ timestamp: "x-mdbase-proof-timestamp",
59
+ nonce: "x-mdbase-proof-nonce",
60
+ signature: "x-mdbase-proof-signature"
61
+ };
62
+ /**
63
+ * Validate a native OAuth callback against the publisher of its web manifest.
64
+ * Reverse-domain private-use schemes keep native app identity tied to the
65
+ * manifest origin; PKCE remains mandatory at the authorization boundary.
66
+ */
67
+ export function isNativeRedirectUri(url, publisherHostname) {
68
+ const scheme = url.protocol.slice(0, -1);
69
+ const publisherPrefix = publisherHostname
70
+ ?.toLowerCase()
71
+ .split(".")
72
+ .reverse()
73
+ .join(".");
74
+ return scheme.includes(".")
75
+ && /^[a-z][a-z0-9+.-]*$/.test(scheme)
76
+ && !["http", "https", "file", "javascript", "data"].includes(scheme)
77
+ && (!publisherPrefix
78
+ || scheme === publisherPrefix
79
+ || scheme.startsWith(`${publisherPrefix}.`))
80
+ && url.username === ""
81
+ && url.password === ""
82
+ && url.hostname.length > 0
83
+ && url.hash === "";
84
+ }
85
+ export const CONNECT_SCHEMA_IDS = {
86
+ appManifest: "https://mdbase.dev/connect/schemas/mdbase-app.v1.json",
87
+ notificationWebhook: "https://mdbase.dev/connect/schemas/notification-webhook.v1.json",
88
+ dataContract: "https://mdbase.dev/schemas/v0.3/data-contract.schema.json",
89
+ eventActionInterop: "https://mdbase.dev/schemas/interop/v0.1/profile.schema.json",
90
+ protocol: "https://mdbase.dev/connect/schemas/connect-protocol.v1.json",
91
+ encryptedRelay: "https://mdbase.dev/connect/schemas/encrypted-relay.v3.json",
92
+ files: "https://mdbase.dev/connect/schemas/files.v1.json",
93
+ sync: "https://mdbase.dev/connect/schemas/sync.v1.json"
94
+ };
@@ -0,0 +1,62 @@
1
+ import type { LegacyMdbaseAppManifest } from "./index.js";
2
+ export type { LegacyApplicationFileRequirement, LegacyApplicationRequirements, LegacyMdbaseWebAppManifest, LegacyMdbasePortableAppManifest, LegacyMdbaseAppManifest } from "./index.js";
3
+ import { type ApplicationNotifications, type ApplicationProvisions, type ApplicationRequirements, type MdbasePortableAppManifest, type MdbaseWebAppManifest } from "./index.js";
4
+ export declare const APPLICATION_MANIFEST_MAX_BYTES = 524288;
5
+ export interface ManifestValidationIssue {
6
+ path: string;
7
+ keyword: string;
8
+ message: string;
9
+ params: Record<string, unknown>;
10
+ }
11
+ export type ManifestValidationResult = {
12
+ valid: true;
13
+ issues: [];
14
+ } | {
15
+ valid: false;
16
+ issues: ManifestValidationIssue[];
17
+ };
18
+ export interface ManifestValidationOptions {
19
+ /** Allow HTTP only for localhost/loopback developer web manifests. */
20
+ allowLocal?: boolean;
21
+ /** Maximum UTF-8 size of the complete declaration. */
22
+ maxBytes?: number;
23
+ }
24
+ interface ValidatedManifestFields {
25
+ requirements: ApplicationRequirements;
26
+ provisions: ApplicationProvisions;
27
+ notifications: ApplicationNotifications;
28
+ }
29
+ export type ValidatedWebAppManifest = Omit<MdbaseWebAppManifest, keyof ValidatedManifestFields> & ValidatedManifestFields;
30
+ export type ValidatedPortableAppManifest = Omit<MdbasePortableAppManifest, keyof ValidatedManifestFields> & ValidatedManifestFields;
31
+ export type ValidatedAppManifest = ValidatedWebAppManifest | ValidatedPortableAppManifest;
32
+ export type ValidatedLegacyAppManifest = LegacyMdbaseAppManifest & {
33
+ provisions: ApplicationProvisions;
34
+ notifications: ApplicationNotifications;
35
+ };
36
+ /** The discriminator describes the original declaration, not a translation. */
37
+ export type VersionedAppManifest = {
38
+ contractVersion: 1;
39
+ manifest: ValidatedLegacyAppManifest;
40
+ } | {
41
+ contractVersion: 2;
42
+ manifest: ValidatedAppManifest;
43
+ };
44
+ export declare class AppManifestValidationError extends Error {
45
+ readonly issues: ManifestValidationIssue[];
46
+ constructor(issues: ManifestValidationIssue[]);
47
+ }
48
+ /**
49
+ * Validate the complete bundled application declaration accepted by Connect.
50
+ *
51
+ * This is the canonical runtime validator used by the server and developer
52
+ * tooling. Keep structural rules in the published JSON Schema and cross-field
53
+ * rules here; consumers must not maintain parallel manifest validators.
54
+ */
55
+ export declare function validateAppManifest(value: unknown, options?: ManifestValidationOptions): ManifestValidationResult;
56
+ /** Explicit opt-in to predecessor declarations; omission defaults to v1. */
57
+ export declare function validateVersionedAppManifest(value: unknown, options?: ManifestValidationOptions): ManifestValidationResult;
58
+ /** Validate, normalize optional sections, and return an isolated declaration. */
59
+ export declare function parseAppManifest(value: unknown, options?: ManifestValidationOptions): ValidatedAppManifest;
60
+ /** Validate and clone without changing capability IDs, file aliases, or version. */
61
+ export declare function parseVersionedAppManifest(value: unknown, options?: ManifestValidationOptions): VersionedAppManifest;
62
+ export declare function formatManifestValidationIssues(issues: readonly ManifestValidationIssue[]): string;
@@ -0,0 +1,527 @@
1
+ import { createHash } from "node:crypto";
2
+ import { Ajv2020 } from "ajv/dist/2020.js";
3
+ import addFormatsModule from "ajv-formats";
4
+ import appManifestSchema from "../schemas/mdbase-app.schema.json" with { type: "json" };
5
+ import legacyAppManifestSchema from "../schemas/mdbase-app.legacy-v1.schema.json" with { type: "json" };
6
+ import { isNativeRedirectUri } from "./index.js";
7
+ export const APPLICATION_MANIFEST_MAX_BYTES = 524_288;
8
+ export class AppManifestValidationError extends Error {
9
+ issues;
10
+ constructor(issues) {
11
+ super(`Invalid application manifest: ${formatManifestValidationIssues(issues)}`);
12
+ this.issues = issues;
13
+ this.name = "AppManifestValidationError";
14
+ }
15
+ }
16
+ const ajv = new Ajv2020({
17
+ allErrors: true,
18
+ strict: true,
19
+ // `required` may name properties declared by an enclosing oneOf branch.
20
+ strictRequired: false
21
+ });
22
+ const addFormats = addFormatsModule;
23
+ addFormats(ajv);
24
+ ajv.addSchema(appManifestSchema);
25
+ const appManifestValidator = requiredValidator(String(appManifestSchema.$id));
26
+ ajv.addSchema(legacyAppManifestSchema);
27
+ const legacyAppManifestValidator = requiredValidator(String(legacyAppManifestSchema.$id));
28
+ /**
29
+ * Validate the complete bundled application declaration accepted by Connect.
30
+ *
31
+ * This is the canonical runtime validator used by the server and developer
32
+ * tooling. Keep structural rules in the published JSON Schema and cross-field
33
+ * rules here; consumers must not maintain parallel manifest validators.
34
+ */
35
+ export function validateAppManifest(value, options = {}) {
36
+ return validateManifest(value, options, false);
37
+ }
38
+ /** Explicit opt-in to predecessor declarations; omission defaults to v1. */
39
+ export function validateVersionedAppManifest(value, options = {}) {
40
+ return validateManifest(value, options, true);
41
+ }
42
+ function declaredContractVersion(value) {
43
+ const requirements = asObject(asObject(value).requirements);
44
+ return requirements.capabilities === undefined
45
+ ? 1
46
+ : asObject(requirements.capabilities).contract_version;
47
+ }
48
+ function validateManifest(value, options, versioned) {
49
+ const sizeIssue = validateSerializedSize(value, options.maxBytes ?? APPLICATION_MANIFEST_MAX_BYTES);
50
+ if (sizeIssue)
51
+ return invalid([sizeIssue]);
52
+ const version = versioned ? declaredContractVersion(value) : 2;
53
+ if (version !== 1 && version !== 2) {
54
+ return invalid([issue("/requirements/capabilities/contract_version", "contractVersion", "must declare supported contract version 1 or 2")]);
55
+ }
56
+ const candidate = options.allowLocal ? localManifestSchemaCandidate(value) : value;
57
+ const schemaResult = validationResult(version === 1 ? legacyAppManifestValidator : appManifestValidator, candidate);
58
+ if (!schemaResult.valid)
59
+ return schemaResult;
60
+ const issues = [
61
+ ...validateManifestOrigins(value, options.allowLocal === true),
62
+ ...(version === 1
63
+ ? validateLegacyCapabilityRequirements(value)
64
+ : validateCapabilityRequirements(value)),
65
+ ...validateProvisionRequirements(value),
66
+ ...validateConfigurationSetup(value)
67
+ ];
68
+ return issues.length === 0 ? { valid: true, issues: [] } : invalid(issues);
69
+ }
70
+ /** Validate, normalize optional sections, and return an isolated declaration. */
71
+ export function parseAppManifest(value, options = {}) {
72
+ const result = validateAppManifest(value, options);
73
+ if (!result.valid)
74
+ throw new AppManifestValidationError(result.issues);
75
+ return normalizeManifest(value);
76
+ }
77
+ /** Validate and clone without changing capability IDs, file aliases, or version. */
78
+ export function parseVersionedAppManifest(value, options = {}) {
79
+ const result = validateVersionedAppManifest(value, options);
80
+ if (!result.valid)
81
+ throw new AppManifestValidationError(result.issues);
82
+ return declaredContractVersion(value) === 1
83
+ ? { contractVersion: 1, manifest: normalizeManifest(value) }
84
+ : { contractVersion: 2, manifest: normalizeManifest(value) };
85
+ }
86
+ function normalizeManifest(value) {
87
+ const manifest = structuredClone(value);
88
+ return {
89
+ ...manifest,
90
+ requirements: {
91
+ configuration: [],
92
+ ...manifest.requirements
93
+ },
94
+ provisions: {
95
+ type_packs: [],
96
+ configuration: [],
97
+ ...manifest.provisions
98
+ },
99
+ notifications: manifest.notifications ?? { criteria: [] }
100
+ };
101
+ }
102
+ export function formatManifestValidationIssues(issues) {
103
+ return issues.map((issue) => `${issue.path || "/"} ${issue.message}`).join("; ");
104
+ }
105
+ function requiredValidator(reference) {
106
+ const validate = ajv.getSchema(reference);
107
+ if (!validate)
108
+ throw new Error(`Canonical schema is unavailable: ${reference}`);
109
+ return validate;
110
+ }
111
+ function validationResult(validate, value) {
112
+ if (validate(value))
113
+ return { valid: true, issues: [] };
114
+ const issues = (validate.errors ?? []).map(validationIssue);
115
+ const specific = issues.filter((issue) => issue.keyword !== "oneOf");
116
+ return invalid(specific.length > 0 ? specific : issues);
117
+ }
118
+ function validationIssue(error) {
119
+ const params = error.params;
120
+ if (error.keyword === "required" && typeof params.missingProperty === "string") {
121
+ return {
122
+ path: joinPointer(error.instancePath, params.missingProperty),
123
+ keyword: error.keyword,
124
+ message: "is required",
125
+ params
126
+ };
127
+ }
128
+ if (error.keyword === "additionalProperties"
129
+ && typeof params.additionalProperty === "string") {
130
+ return {
131
+ path: joinPointer(error.instancePath, params.additionalProperty),
132
+ keyword: error.keyword,
133
+ message: "is not allowed",
134
+ params
135
+ };
136
+ }
137
+ return {
138
+ path: error.instancePath,
139
+ keyword: error.keyword,
140
+ message: error.message ?? "is invalid",
141
+ params
142
+ };
143
+ }
144
+ function joinPointer(parent, child) {
145
+ const escaped = child.replaceAll("~", "~0").replaceAll("/", "~1");
146
+ return `${parent}/${escaped}`;
147
+ }
148
+ function validateSerializedSize(value, maxBytes) {
149
+ const jsonIssue = validateJsonValue(value, "", new WeakSet());
150
+ if (jsonIssue)
151
+ return jsonIssue;
152
+ try {
153
+ const serialized = JSON.stringify(value);
154
+ const bytes = new TextEncoder().encode(serialized).byteLength;
155
+ return bytes <= maxBytes
156
+ ? undefined
157
+ : issue("/", "maxBytes", `must not exceed ${maxBytes} UTF-8 bytes`, { maxBytes, actualBytes: bytes });
158
+ }
159
+ catch {
160
+ return issue("/", "json", "must be serializable as JSON");
161
+ }
162
+ }
163
+ function validateJsonValue(value, path, ancestors) {
164
+ if (value === null
165
+ || typeof value === "string"
166
+ || typeof value === "boolean"
167
+ || (typeof value === "number" && Number.isFinite(value)))
168
+ return undefined;
169
+ if (typeof value !== "object") {
170
+ return issue(path || "/", "json", "must be a JSON value");
171
+ }
172
+ if (ancestors.has(value)) {
173
+ return issue(path || "/", "json", "must not contain circular references");
174
+ }
175
+ const prototype = Object.getPrototypeOf(value);
176
+ if (!Array.isArray(value) && prototype !== Object.prototype && prototype !== null) {
177
+ return issue(path || "/", "json", "must be a plain JSON object");
178
+ }
179
+ ancestors.add(value);
180
+ const entries = Array.isArray(value)
181
+ ? value.map((item, index) => [String(index), item])
182
+ : Object.entries(value);
183
+ for (const [key, child] of entries) {
184
+ const childIssue = validateJsonValue(child, joinPointer(path, key), ancestors);
185
+ if (childIssue)
186
+ return childIssue;
187
+ }
188
+ ancestors.delete(value);
189
+ return undefined;
190
+ }
191
+ function validateManifestOrigins(value, allowLocal) {
192
+ const manifest = asObject(value);
193
+ if (manifest.distribution === "portable") {
194
+ if (manifest.project_url === undefined) {
195
+ return manifest.icon === undefined
196
+ ? []
197
+ : [issue("/icon", "origin", "requires a project_url on the same origin")];
198
+ }
199
+ try {
200
+ const project = new URL(String(manifest.project_url));
201
+ if (project.protocol !== "https:") {
202
+ return [issue("/project_url", "protocol", "must use HTTPS")];
203
+ }
204
+ if (manifest.icon !== undefined
205
+ && new URL(String(manifest.icon)).origin !== project.origin) {
206
+ return [issue("/icon", "origin", "must use the project_url origin")];
207
+ }
208
+ return [];
209
+ }
210
+ catch {
211
+ return [issue("/", "url", "contains an invalid URL")];
212
+ }
213
+ }
214
+ try {
215
+ const homepage = new URL(String(manifest.homepage));
216
+ if (!secureOrAllowedLocal(homepage, allowLocal)) {
217
+ return [issue("/homepage", "protocol", "must use HTTPS (or loopback HTTP in local mode)")];
218
+ }
219
+ for (const [index, redirect] of manifest.redirect_uris.entries()) {
220
+ const url = new URL(String(redirect));
221
+ if (url.origin === homepage.origin && !secureOrAllowedLocal(url, allowLocal)) {
222
+ return [issue(`/redirect_uris/${index}`, "protocol", "must use HTTPS (or loopback HTTP in local mode)")];
223
+ }
224
+ const nativeAllowed = nativeRedirectMatchesApplication(url, String(manifest.id));
225
+ if (url.origin !== homepage.origin && !nativeAllowed) {
226
+ return [issue(`/redirect_uris/${index}`, "origin", "must use the homepage origin or a private-use scheme matching the application ID")];
227
+ }
228
+ }
229
+ if (manifest.icon !== undefined
230
+ && new URL(String(manifest.icon)).origin !== homepage.origin) {
231
+ return [issue("/icon", "origin", "must use the homepage origin")];
232
+ }
233
+ return [];
234
+ }
235
+ catch {
236
+ return [issue("/", "url", "contains an invalid URL")];
237
+ }
238
+ }
239
+ function nativeRedirectMatchesApplication(url, applicationId) {
240
+ const scheme = url.protocol.slice(0, -1);
241
+ return isNativeRedirectUri(url)
242
+ && (scheme === applicationId || scheme.startsWith(`${applicationId}.`));
243
+ }
244
+ function validateProvisionRequirements(value) {
245
+ const manifest = asObject(value);
246
+ const requirements = asObject(manifest.requirements);
247
+ const requiredContracts = Array.isArray(requirements.contracts)
248
+ ? requirements.contracts
249
+ : [];
250
+ const required = new Set(requiredContracts.map((contract) => {
251
+ const value = asObject(contract);
252
+ return `${value.id}@${value.version}`;
253
+ }));
254
+ const provisions = asObject(manifest.provisions);
255
+ const packs = Array.isArray(provisions.type_packs) ? provisions.type_packs : [];
256
+ const issues = [];
257
+ for (const [packIndex, provisionValue] of packs.entries()) {
258
+ const provision = asObject(provisionValue);
259
+ const providedContracts = Array.isArray(provision.provides)
260
+ ? provision.provides
261
+ : [];
262
+ for (const providedValue of providedContracts) {
263
+ const provided = asObject(providedValue);
264
+ if (!required.has(`${provided.id}@${provided.version}`)) {
265
+ issues.push(issue(`/provisions/type_packs/${packIndex}/provides`, "contractRequirement", "may only contain contracts required by the application"));
266
+ }
267
+ }
268
+ const packManifest = asObject(provision.manifest);
269
+ const declaredResources = Array.isArray(packManifest.resources)
270
+ ? packManifest.resources.map(asObject)
271
+ : [];
272
+ const embeddedResources = Array.isArray(provision.resources)
273
+ ? provision.resources.map(asObject)
274
+ : [];
275
+ const embedded = new Map(embeddedResources.map((resource) => [String(resource.source), resource.document]));
276
+ if (embedded.size !== embeddedResources.length
277
+ || new Set(declaredResources.map((resource) => String(resource.source))).size
278
+ !== declaredResources.length
279
+ || declaredResources.length !== embeddedResources.length) {
280
+ issues.push(issue(`/provisions/type_packs/${packIndex}/resources`, "resourceSet", "must match manifest source paths exactly"));
281
+ continue;
282
+ }
283
+ for (const [resourceIndex, resource] of declaredResources.entries()) {
284
+ const source = String(resource.source);
285
+ const document = embedded.get(source);
286
+ if (typeof document !== "string") {
287
+ issues.push(issue(`/provisions/type_packs/${packIndex}/resources`, "resourceSet", `is missing manifest source ${source}`));
288
+ continue;
289
+ }
290
+ const digest = `sha256:${createHash("sha256").update(document).digest("hex")}`;
291
+ if (digest !== resource.digest) {
292
+ issues.push(issue(`/provisions/type_packs/${packIndex}/manifest/resources/${resourceIndex}/digest`, "digest", "does not match the embedded document"));
293
+ }
294
+ }
295
+ }
296
+ return issues;
297
+ }
298
+ function validateConfigurationSetup(value) {
299
+ const manifest = asObject(value);
300
+ const requirements = asObject(manifest.requirements);
301
+ const provisions = asObject(manifest.provisions);
302
+ const requiredValues = Array.isArray(requirements.configuration)
303
+ ? requirements.configuration
304
+ : [];
305
+ const provisionValues = Array.isArray(provisions.configuration)
306
+ ? provisions.configuration
307
+ : [];
308
+ const issues = [];
309
+ const required = new Map();
310
+ for (const [index, candidate] of requiredValues.entries()) {
311
+ const requirement = asObject(candidate);
312
+ const id = String(requirement.id);
313
+ const path = String(requirement.path);
314
+ const pathError = configurationPointerError(path);
315
+ if (pathError) {
316
+ issues.push(issue(`/requirements/configuration/${index}/path`, "configurationPointer", pathError));
317
+ }
318
+ if (required.has(id)) {
319
+ issues.push(issue(`/requirements/configuration/${index}/id`, "uniqueRequirement", `duplicates configuration requirement ${id}`));
320
+ }
321
+ else {
322
+ required.set(id, { path, value: requirement.value, index });
323
+ }
324
+ }
325
+ const linked = new Set();
326
+ const contributionKeys = new Set();
327
+ for (const [index, candidate] of provisionValues.entries()) {
328
+ const provision = asObject(candidate);
329
+ const requirementId = String(provision.requirement);
330
+ const path = String(provision.path);
331
+ const pathError = configurationPointerError(path);
332
+ if (pathError) {
333
+ issues.push(issue(`/provisions/configuration/${index}/path`, "configurationPointer", pathError));
334
+ }
335
+ const requirement = required.get(requirementId);
336
+ if (!requirement) {
337
+ issues.push(issue(`/provisions/configuration/${index}/requirement`, "configurationRequirement", `references unknown configuration requirement ${requirementId}`));
338
+ continue;
339
+ }
340
+ if (linked.has(requirementId)) {
341
+ issues.push(issue(`/provisions/configuration/${index}/requirement`, "uniqueProvision", `configuration requirement ${requirementId} may have exactly one provision`));
342
+ }
343
+ linked.add(requirementId);
344
+ if (path !== requirement.path) {
345
+ issues.push(issue(`/provisions/configuration/${index}/path`, "configurationRequirement", `must equal /requirements/configuration/${requirement.index}/path`));
346
+ }
347
+ if (JSON.stringify(provision.value) !== JSON.stringify(requirement.value)) {
348
+ issues.push(issue(`/provisions/configuration/${index}/value`, "configurationRequirement", `must equal /requirements/configuration/${requirement.index}/value`));
349
+ }
350
+ const contributionKey = `${path}\0${JSON.stringify(provision.value)}`;
351
+ if (contributionKeys.has(contributionKey)) {
352
+ issues.push(issue(`/provisions/configuration/${index}`, "uniqueContribution", "duplicates another configuration path and value contribution"));
353
+ }
354
+ contributionKeys.add(contributionKey);
355
+ }
356
+ for (const [id, requirement] of required) {
357
+ if (!linked.has(id)) {
358
+ issues.push(issue(`/requirements/configuration/${requirement.index}`, "configurationProvision", `requires one provision linked by id ${id}`));
359
+ }
360
+ }
361
+ return issues;
362
+ }
363
+ function configurationPointerError(path) {
364
+ if (!path.startsWith("/") || new TextEncoder().encode(path).byteLength > 1024) {
365
+ return "must be a bounded RFC 6901 JSON pointer";
366
+ }
367
+ const encoded = path.slice(1).split("/");
368
+ if (encoded.length < 2 || encoded.length > 16) {
369
+ return "must address a value below an x-* extension namespace";
370
+ }
371
+ const segments = [];
372
+ for (const value of encoded) {
373
+ if (/~(?:[^01]|$)/u.test(value))
374
+ return "contains an invalid RFC 6901 escape";
375
+ const segment = value.replaceAll("~1", "/").replaceAll("~0", "~");
376
+ if (segment.length === 0
377
+ || segment === "-"
378
+ || /^\d+$/u.test(segment)
379
+ || [...segment].some((character) => /\p{Cc}/u.test(character)))
380
+ return "contains a disallowed object-key segment";
381
+ segments.push(segment);
382
+ }
383
+ return /^x-[a-z0-9][a-z0-9-]*$/u.test(segments[0])
384
+ ? undefined
385
+ : "must be inside a top-level x-* extension namespace";
386
+ }
387
+ function validateCapabilityRequirements(value) {
388
+ const manifest = asObject(value);
389
+ const requirements = asObject(manifest.requirements);
390
+ const capabilities = asObject(requirements.capabilities);
391
+ const issues = [];
392
+ if (requirements.access !== "full_collection") {
393
+ issues.push(issue("/requirements/access", "collectionAccess", "must explicitly request full_collection; legacy or omitted access is not widened"));
394
+ }
395
+ const required = Array.isArray(capabilities.required)
396
+ ? capabilities.required.map(String)
397
+ : [];
398
+ const optional = Array.isArray(capabilities.optional)
399
+ ? capabilities.optional.map(String)
400
+ : [];
401
+ const overlap = optional.find((capability) => required.includes(capability));
402
+ if (overlap) {
403
+ issues.push(issue("/requirements/capabilities/optional", "disjoint", `must not repeat required capability ${overlap}`));
404
+ }
405
+ const files = asObject(requirements.files);
406
+ const requiredFiles = Array.isArray(files.required) ? files.required.map(String) : [];
407
+ const optionalFiles = Array.isArray(files.optional) ? files.optional.map(String) : [];
408
+ const fileOverlap = optionalFiles.find((action) => requiredFiles.includes(action));
409
+ if (fileOverlap) {
410
+ issues.push(issue("/requirements/files/optional", "disjoint", `must not repeat required file action ${fileOverlap}`));
411
+ }
412
+ return issues;
413
+ }
414
+ // Frozen cross-field rules from c2596a6e; notably, absent capabilities skip
415
+ // alias/setup checks rather than inventing or widening a declaration.
416
+ function validateLegacyCapabilityRequirements(value) {
417
+ const manifest = asObject(value);
418
+ const requirements = asObject(manifest.requirements);
419
+ const capabilities = asObject(requirements.capabilities);
420
+ const issues = [];
421
+ if (requirements.access !== "full_collection") {
422
+ issues.push(issue("/requirements/access", "collectionAccess", "must explicitly request full_collection; legacy or omitted access is not widened"));
423
+ }
424
+ if (Object.keys(capabilities).length === 0)
425
+ return issues;
426
+ const required = Array.isArray(capabilities.required)
427
+ ? capabilities.required.map(String)
428
+ : [];
429
+ const optional = Array.isArray(capabilities.optional)
430
+ ? capabilities.optional.map(String)
431
+ : [];
432
+ const overlap = optional.find((capability) => required.includes(capability));
433
+ if (overlap) {
434
+ issues.push(issue("/requirements/capabilities/optional", "disjoint", `must not repeat required capability ${overlap}`));
435
+ }
436
+ const declared = new Set([...required, ...optional]);
437
+ const provisions = asObject(manifest.provisions);
438
+ if (declared.has("definitions.type-pack.apply")) {
439
+ if (requirements.access !== "full_collection") {
440
+ issues.push(issue("/requirements/access", "typePackAccess", "must be full_collection for definitions.type-pack.apply"));
441
+ }
442
+ }
443
+ if (Array.isArray(provisions.type_packs)
444
+ && provisions.type_packs.length > 0
445
+ && !required.includes("collection.setup.apply")) {
446
+ issues.push(issue("/requirements/capabilities/required", "collectionSetupCapability", "must require collection.setup.apply when bundled type packs are declared"));
447
+ }
448
+ const hasConfigurationProvisions = Array.isArray(provisions.configuration)
449
+ && provisions.configuration.length > 0;
450
+ const hasSetupProvisions = hasConfigurationProvisions
451
+ || (Array.isArray(provisions.type_packs) && provisions.type_packs.length > 0);
452
+ if (hasConfigurationProvisions && !required.includes("collection.setup.apply")) {
453
+ issues.push(issue("/requirements/capabilities/required", "collectionSetupCapability", "must require collection.setup.apply when configuration provisions are declared"));
454
+ }
455
+ if (declared.has("collection.setup.apply")) {
456
+ if (requirements.access !== "full_collection") {
457
+ issues.push(issue("/requirements/access", "collectionSetupAccess", "must be full_collection for collection.setup.apply"));
458
+ }
459
+ if (!hasSetupProvisions) {
460
+ issues.push(issue("/provisions", "collectionSetupProvision", "must declare a configuration provision or type pack for collection.setup.apply"));
461
+ }
462
+ }
463
+ if (declared.has("notifications.background-delivery")
464
+ && !Array.isArray(asObject(manifest.notifications).criteria)) {
465
+ issues.push(issue("/notifications/criteria", "notificationCapability", "must be declared for notifications.background-delivery"));
466
+ }
467
+ const fileRequirement = asObject(requirements.files);
468
+ const fileActions = Array.isArray(fileRequirement.actions)
469
+ ? new Set(fileRequirement.actions.map(String))
470
+ : new Set();
471
+ for (const action of ["list", "read", "add", "replace", "move", "delete"]) {
472
+ const capability = `files.${action}`;
473
+ if (declared.has(capability) !== fileActions.has(action)) {
474
+ issues.push(issue("/requirements/capabilities", "fileCapability", `${capability} and requirements.files.actions.${action} must be declared together`));
475
+ }
476
+ }
477
+ return issues;
478
+ }
479
+ function localManifestSchemaCandidate(value) {
480
+ const source = asObject(value);
481
+ if (source.distribution === "portable")
482
+ return value;
483
+ // Only URL fields need a schema-safe local substitute. A shallow copy keeps
484
+ // non-JSON values visible to the schema instead of silently dropping them.
485
+ const object = { ...source };
486
+ for (const field of ["homepage", "icon"]) {
487
+ if (typeof object[field] === "string") {
488
+ object[field] = schemaSafeLocalUrl(object[field]);
489
+ }
490
+ }
491
+ if (Array.isArray(object.redirect_uris)) {
492
+ object.redirect_uris = object.redirect_uris.map((url) => typeof url === "string" ? schemaSafeLocalUrl(url) : url);
493
+ }
494
+ return object;
495
+ }
496
+ function schemaSafeLocalUrl(value) {
497
+ try {
498
+ const url = new URL(value);
499
+ if (url.protocol === "http:" && isLoopback(url.hostname))
500
+ url.protocol = "https:";
501
+ return url.href;
502
+ }
503
+ catch {
504
+ return value;
505
+ }
506
+ }
507
+ function secureOrAllowedLocal(url, allowLocal) {
508
+ return url.protocol === "https:"
509
+ || (allowLocal && url.protocol === "http:" && isLoopback(url.hostname));
510
+ }
511
+ function isLoopback(hostname) {
512
+ return hostname === "localhost"
513
+ || hostname === "127.0.0.1"
514
+ || hostname === "[::1]"
515
+ || hostname === "::1";
516
+ }
517
+ function invalid(issues) {
518
+ return { valid: false, issues };
519
+ }
520
+ function issue(path, keyword, message, params = {}) {
521
+ return { path, keyword, message, params };
522
+ }
523
+ function asObject(value) {
524
+ return value !== null && typeof value === "object" && !Array.isArray(value)
525
+ ? value
526
+ : {};
527
+ }