@skenora/resources 0.1.2

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 (46) hide show
  1. package/README.md +60 -0
  2. package/dist/http.d.ts +2 -0
  3. package/dist/http.d.ts.map +1 -0
  4. package/dist/http.js +2 -0
  5. package/dist/http.js.map +1 -0
  6. package/dist/index.d.ts +4 -0
  7. package/dist/index.d.ts.map +1 -0
  8. package/dist/index.js +4 -0
  9. package/dist/index.js.map +1 -0
  10. package/dist/memory.d.ts +2 -0
  11. package/dist/memory.d.ts.map +1 -0
  12. package/dist/memory.js +2 -0
  13. package/dist/memory.js.map +1 -0
  14. package/dist/providers/http-provider.d.ts +22 -0
  15. package/dist/providers/http-provider.d.ts.map +1 -0
  16. package/dist/providers/http-provider.js +157 -0
  17. package/dist/providers/http-provider.js.map +1 -0
  18. package/dist/providers/memory-provider.d.ts +19 -0
  19. package/dist/providers/memory-provider.d.ts.map +1 -0
  20. package/dist/providers/memory-provider.js +131 -0
  21. package/dist/providers/memory-provider.js.map +1 -0
  22. package/dist/providers/workspace-provider.d.ts +14 -0
  23. package/dist/providers/workspace-provider.d.ts.map +1 -0
  24. package/dist/providers/workspace-provider.js +126 -0
  25. package/dist/providers/workspace-provider.js.map +1 -0
  26. package/dist/resource-bindings.d.ts +94 -0
  27. package/dist/resource-bindings.d.ts.map +1 -0
  28. package/dist/resource-bindings.js +546 -0
  29. package/dist/resource-bindings.js.map +1 -0
  30. package/dist/resource-manager.d.ts +29 -0
  31. package/dist/resource-manager.d.ts.map +1 -0
  32. package/dist/resource-manager.js +380 -0
  33. package/dist/resource-manager.js.map +1 -0
  34. package/dist/types.d.ts +144 -0
  35. package/dist/types.d.ts.map +1 -0
  36. package/dist/types.js +30 -0
  37. package/dist/types.js.map +1 -0
  38. package/dist/workspace-resource-context.d.ts +21 -0
  39. package/dist/workspace-resource-context.d.ts.map +1 -0
  40. package/dist/workspace-resource-context.js +22 -0
  41. package/dist/workspace-resource-context.js.map +1 -0
  42. package/dist/workspace.d.ts +3 -0
  43. package/dist/workspace.d.ts.map +1 -0
  44. package/dist/workspace.js +3 -0
  45. package/dist/workspace.js.map +1 -0
  46. package/package.json +47 -0
@@ -0,0 +1,94 @@
1
+ import { type AssetReference, type ResourceIntegrity } from "@skenora/contracts";
2
+ import type { ResolvedResource, ResourceAcquireOptions, ResourceDescriptor, ResourceLicenseMetadata, ResourceResolver, ResourceScopeOptions, ResourceTechnicalMetadata } from "./types.js";
3
+ export type LogicalAssetRef = string;
4
+ export interface ResourceBindingRequirement {
5
+ readonly assetRef: LogicalAssetRef;
6
+ readonly kind: AssetReference["kind"];
7
+ readonly mediaType?: string;
8
+ readonly integrity?: ResourceIntegrity;
9
+ }
10
+ /**
11
+ * A host-created binding from a plan-level logical reference to an authorized
12
+ * scene asset. The resources package does not perform authorization; callers
13
+ * must only provide references that have already passed host policy.
14
+ */
15
+ export interface ResourceBinding {
16
+ readonly assetRef: LogicalAssetRef;
17
+ readonly asset: Readonly<AssetReference>;
18
+ }
19
+ export type ResourceBindingIssueCode = "invalid-asset-ref" | "duplicate-asset-ref" | "duplicate-requirement" | "missing-binding" | "unexpected-binding" | "invalid-asset" | "missing-source" | "invalid-kind" | "invalid-media-type" | "kind-mismatch" | "media-type-mismatch" | "invalid-integrity" | "integrity-mismatch";
20
+ export interface ResourceBindingIssue {
21
+ readonly code: ResourceBindingIssueCode;
22
+ readonly path: readonly (string | number)[];
23
+ readonly message: string;
24
+ readonly assetRef?: LogicalAssetRef;
25
+ }
26
+ export interface ResourceBindingValidationResult {
27
+ readonly valid: boolean;
28
+ readonly issues: readonly ResourceBindingIssue[];
29
+ }
30
+ export interface ResourceBindingValidationOptions {
31
+ /** Reject bindings not declared by the supplied requirements. */
32
+ readonly allowAdditionalBindings?: boolean;
33
+ }
34
+ export interface ResourceBindingTable {
35
+ readonly size: number;
36
+ readonly bindings: readonly ResourceBinding[];
37
+ get(assetRef: LogicalAssetRef): ResourceBinding | undefined;
38
+ require(assetRef: LogicalAssetRef): ResourceBinding;
39
+ }
40
+ export declare class ResourceBindingValidationError extends Error {
41
+ readonly issues: readonly ResourceBindingIssue[];
42
+ constructor(issues: readonly ResourceBindingIssue[]);
43
+ }
44
+ /** Validates plan requirements without acquiring resources or performing I/O. */
45
+ export declare function validateResourceBindings(bindings: readonly Readonly<ResourceBinding>[], requirements?: readonly Readonly<ResourceBindingRequirement>[], options?: Readonly<ResourceBindingValidationOptions>): ResourceBindingValidationResult;
46
+ /**
47
+ * Creates an immutable lookup table for a validated binding set. Asset values
48
+ * are cloned and frozen so a plan execution cannot observe caller mutation.
49
+ */
50
+ export declare function createResourceBindingTable(bindings: readonly Readonly<ResourceBinding>[], requirements?: readonly Readonly<ResourceBindingRequirement>[], options?: Readonly<ResourceBindingValidationOptions>): ResourceBindingTable;
51
+ export interface ModelVisibleResourceDescriptor {
52
+ /** Opaque logical reference selected by the host, never a provider id. */
53
+ readonly resourceRef: LogicalAssetRef;
54
+ readonly label: string;
55
+ readonly kind: AssetReference["kind"];
56
+ readonly description?: string;
57
+ readonly tags?: readonly string[];
58
+ readonly mediaType?: string;
59
+ readonly integrity?: ResourceIntegrity;
60
+ readonly technical?: ResourceTechnicalMetadata;
61
+ readonly license?: ResourceLicenseMetadata;
62
+ }
63
+ /**
64
+ * Projects a provider descriptor into a model-visible DTO. Source, thumbnail,
65
+ * provider id, and every locator-bearing field are intentionally omitted.
66
+ */
67
+ export declare function toModelVisibleResourceDescriptor(descriptor: Readonly<ResourceDescriptor>, resourceRef: LogicalAssetRef): ModelVisibleResourceDescriptor;
68
+ export interface ResourceLease {
69
+ readonly assetRef: LogicalAssetRef;
70
+ /** Internal authorized reference; never pass this object to the model. */
71
+ readonly asset: Readonly<AssetReference>;
72
+ /** Resolves while the lease remains retained; release invalidates the URL. */
73
+ readonly resource: Promise<ResolvedResource>;
74
+ /** Idempotently releases this lease and its underlying ResourceScope. */
75
+ release(): void;
76
+ }
77
+ export interface ResourceBindingScopeOptions extends ResourceScopeOptions {
78
+ readonly requirements?: readonly Readonly<ResourceBindingRequirement>[];
79
+ readonly validation?: Readonly<ResourceBindingValidationOptions>;
80
+ }
81
+ export interface ResourceBindingScope {
82
+ readonly bindings: ResourceBindingTable;
83
+ /** Starts acquisition and returns a release handle before provider resolve. */
84
+ acquire(assetRef: LogicalAssetRef, options?: ResourceAcquireOptions): ResourceLease;
85
+ acquireAll(assetRefs: readonly LogicalAssetRef[], options?: ResourceAcquireOptions): readonly ResourceLease[];
86
+ dispose(): void;
87
+ }
88
+ /**
89
+ * Coordinates one ResourceScope per logical lease. Releasing a lease or
90
+ * disposing this scope delegates to the existing reference-counted cache and
91
+ * Blob URL cleanup.
92
+ */
93
+ export declare function createResourceBindingScope(resolver: ResourceResolver, bindings: ResourceBindingTable | readonly Readonly<ResourceBinding>[], options?: Readonly<ResourceBindingScopeOptions>): ResourceBindingScope;
94
+ //# sourceMappingURL=resource-bindings.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resource-bindings.d.ts","sourceRoot":"","sources":["../src/resource-bindings.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,546 @@
1
+ import { getAssetResourceLocator, } from "@skenora/contracts";
2
+ export class ResourceBindingValidationError extends Error {
3
+ issues;
4
+ constructor(issues) {
5
+ const ownedIssues = issues.map((issue) => Object.freeze({
6
+ ...issue,
7
+ path: Object.freeze([...issue.path]),
8
+ }));
9
+ super(ownedIssues.map(formatIssue).join("; "));
10
+ this.name = "ResourceBindingValidationError";
11
+ this.issues = Object.freeze(ownedIssues);
12
+ }
13
+ }
14
+ /** Validates plan requirements without acquiring resources or performing I/O. */
15
+ export function validateResourceBindings(bindings, requirements = [], options = {}) {
16
+ const issues = [];
17
+ const bindingByRef = new Map();
18
+ const requirementByRef = new Map();
19
+ requirements.forEach((requirement, index) => {
20
+ const assetRef = requirement.assetRef;
21
+ if (!isNormalizedAssetRef(assetRef)) {
22
+ issues.push({
23
+ code: "invalid-asset-ref",
24
+ path: ["requirements", index, "assetRef"],
25
+ message: "Resource requirement assetRef must be normalized",
26
+ ...(typeof assetRef === "string" ? { assetRef } : {}),
27
+ });
28
+ }
29
+ else if (requirementByRef.has(assetRef)) {
30
+ issues.push({
31
+ code: "duplicate-requirement",
32
+ path: ["requirements", index, "assetRef"],
33
+ message: "Resource requirement assetRef must be unique",
34
+ assetRef,
35
+ });
36
+ }
37
+ else {
38
+ requirementByRef.set(assetRef, index);
39
+ }
40
+ validateRequirement(requirement, index, issues);
41
+ });
42
+ bindings.forEach((binding, index) => {
43
+ const assetRef = binding.assetRef;
44
+ if (!isNormalizedAssetRef(assetRef)) {
45
+ issues.push({
46
+ code: "invalid-asset-ref",
47
+ path: ["bindings", index, "assetRef"],
48
+ message: "Resource binding assetRef must be normalized",
49
+ ...(typeof assetRef === "string" ? { assetRef } : {}),
50
+ });
51
+ }
52
+ else if (bindingByRef.has(assetRef)) {
53
+ issues.push({
54
+ code: "duplicate-asset-ref",
55
+ path: ["bindings", index, "assetRef"],
56
+ message: "Resource binding assetRef must be unique",
57
+ assetRef,
58
+ });
59
+ }
60
+ else {
61
+ bindingByRef.set(assetRef, index);
62
+ }
63
+ validateBinding(binding, index, issues);
64
+ const requirementIndex = typeof assetRef === "string" ? requirementByRef.get(assetRef) : undefined;
65
+ if (requirementIndex !== undefined) {
66
+ validateBindingAgainstRequirement(binding, requirements[requirementIndex], index, issues);
67
+ }
68
+ else if (requirements.length > 0 &&
69
+ !options.allowAdditionalBindings &&
70
+ isNormalizedAssetRef(assetRef)) {
71
+ issues.push({
72
+ code: "unexpected-binding",
73
+ path: ["bindings", index, "assetRef"],
74
+ message: "Resource binding was not declared by the plan",
75
+ assetRef,
76
+ });
77
+ }
78
+ });
79
+ requirements.forEach((requirement, index) => {
80
+ if (isNormalizedAssetRef(requirement.assetRef) &&
81
+ !bindingByRef.has(requirement.assetRef)) {
82
+ issues.push({
83
+ code: "missing-binding",
84
+ path: ["requirements", index, "assetRef"],
85
+ message: "Resource requirement has no authorized binding",
86
+ assetRef: requirement.assetRef,
87
+ });
88
+ }
89
+ });
90
+ return {
91
+ valid: issues.length === 0,
92
+ issues: Object.freeze(issues.map(ownIssue)),
93
+ };
94
+ }
95
+ /**
96
+ * Creates an immutable lookup table for a validated binding set. Asset values
97
+ * are cloned and frozen so a plan execution cannot observe caller mutation.
98
+ */
99
+ export function createResourceBindingTable(bindings, requirements = [], options = {}) {
100
+ const validation = validateResourceBindings(bindings, requirements, options);
101
+ if (!validation.valid) {
102
+ throw new ResourceBindingValidationError(validation.issues);
103
+ }
104
+ const ownedBindings = Object.freeze(bindings.map((binding) => Object.freeze({
105
+ assetRef: binding.assetRef,
106
+ asset: freezeValue(structuredClone(binding.asset)),
107
+ })));
108
+ const bindingByRef = new Map(ownedBindings.map((binding) => [binding.assetRef, binding]));
109
+ return Object.freeze({
110
+ size: ownedBindings.length,
111
+ bindings: ownedBindings,
112
+ get(assetRef) {
113
+ return bindingByRef.get(assetRef);
114
+ },
115
+ require(assetRef) {
116
+ if (!isNormalizedAssetRef(assetRef)) {
117
+ throw new ResourceBindingValidationError([
118
+ {
119
+ code: "invalid-asset-ref",
120
+ path: ["assetRef"],
121
+ message: "Resource binding assetRef must be normalized",
122
+ ...(typeof assetRef === "string" ? { assetRef } : {}),
123
+ },
124
+ ]);
125
+ }
126
+ const binding = bindingByRef.get(assetRef);
127
+ if (!binding) {
128
+ throw new ResourceBindingValidationError([
129
+ {
130
+ code: "missing-binding",
131
+ path: ["assetRef"],
132
+ message: "Resource binding was not provided",
133
+ assetRef,
134
+ },
135
+ ]);
136
+ }
137
+ return binding;
138
+ },
139
+ });
140
+ }
141
+ /**
142
+ * Projects a provider descriptor into a model-visible DTO. Source, thumbnail,
143
+ * provider id, and every locator-bearing field are intentionally omitted.
144
+ */
145
+ export function toModelVisibleResourceDescriptor(descriptor, resourceRef) {
146
+ assertOpaqueResourceRef(resourceRef);
147
+ assertAssetKind(descriptor.kind);
148
+ if (!descriptor.label.trim()) {
149
+ throw new Error("Resource descriptor label must be non-empty");
150
+ }
151
+ if (descriptor.mediaType !== undefined) {
152
+ assertMediaType(descriptor.mediaType);
153
+ }
154
+ if (descriptor.integrity !== undefined) {
155
+ assertIntegrity(descriptor.integrity);
156
+ }
157
+ const safeDescriptor = {
158
+ resourceRef,
159
+ label: descriptor.label,
160
+ kind: descriptor.kind,
161
+ ...(descriptor.description !== undefined
162
+ ? { description: descriptor.description }
163
+ : {}),
164
+ ...(descriptor.tags !== undefined ? { tags: descriptor.tags } : {}),
165
+ ...(descriptor.mediaType !== undefined
166
+ ? { mediaType: descriptor.mediaType }
167
+ : {}),
168
+ ...(descriptor.integrity !== undefined
169
+ ? { integrity: descriptor.integrity }
170
+ : {}),
171
+ ...(descriptor.technical !== undefined
172
+ ? { technical: descriptor.technical }
173
+ : {}),
174
+ ...(descriptor.license !== undefined
175
+ ? { license: descriptor.license }
176
+ : {}),
177
+ };
178
+ return freezeValue(structuredClone(safeDescriptor));
179
+ }
180
+ /**
181
+ * Coordinates one ResourceScope per logical lease. Releasing a lease or
182
+ * disposing this scope delegates to the existing reference-counted cache and
183
+ * Blob URL cleanup.
184
+ */
185
+ export function createResourceBindingScope(resolver, bindings, options = {}) {
186
+ const table = "bindings" in bindings
187
+ ? validateTable(bindings, options)
188
+ : createResourceBindingTable(bindings, options.requirements ?? [], options.validation ?? {});
189
+ return new ManagedResourceBindingScope(resolver, table, options.signal);
190
+ }
191
+ class ManagedResourceBindingScope {
192
+ bindings;
193
+ #resolver;
194
+ #signal;
195
+ #releases = new Set();
196
+ #abortListener;
197
+ #disposed = false;
198
+ constructor(resolver, bindings, signal) {
199
+ this.#resolver = resolver;
200
+ this.bindings = bindings;
201
+ this.#signal = signal;
202
+ if (!signal) {
203
+ this.#abortListener = null;
204
+ }
205
+ else {
206
+ const listener = () => this.dispose();
207
+ this.#abortListener = listener;
208
+ if (signal.aborted)
209
+ this.#disposed = true;
210
+ else
211
+ signal.addEventListener("abort", listener, { once: true });
212
+ }
213
+ }
214
+ acquire(assetRef, options = {}) {
215
+ this.#assertActive();
216
+ const binding = this.bindings.require(assetRef);
217
+ const acquireOptions = bindingAcquireOptions(binding, options);
218
+ const resourceScope = this.#resolver.createScope(this.#signal ? { signal: this.#signal } : {});
219
+ let released = false;
220
+ const release = () => {
221
+ if (released)
222
+ return;
223
+ released = true;
224
+ this.#releases.delete(release);
225
+ resourceScope.dispose();
226
+ };
227
+ this.#releases.add(release);
228
+ let resource;
229
+ try {
230
+ resource = resourceScope
231
+ .acquire(binding.asset, acquireOptions)
232
+ .then((resolved) => {
233
+ assertResolvedResourceMediaType(binding.assetRef, binding.asset, resolved);
234
+ return resolved;
235
+ })
236
+ .catch((error) => {
237
+ release();
238
+ throw error;
239
+ });
240
+ }
241
+ catch (error) {
242
+ release();
243
+ throw error;
244
+ }
245
+ return Object.freeze({
246
+ assetRef: binding.assetRef,
247
+ asset: binding.asset,
248
+ resource,
249
+ release,
250
+ });
251
+ }
252
+ acquireAll(assetRefs, options = {}) {
253
+ this.#assertActive();
254
+ const leases = [];
255
+ try {
256
+ for (const assetRef of assetRefs)
257
+ leases.push(this.acquire(assetRef, options));
258
+ }
259
+ catch (error) {
260
+ for (const lease of leases)
261
+ lease.release();
262
+ throw error;
263
+ }
264
+ return Object.freeze(leases);
265
+ }
266
+ dispose() {
267
+ if (this.#disposed)
268
+ return;
269
+ this.#disposed = true;
270
+ if (this.#signal && this.#abortListener) {
271
+ this.#signal.removeEventListener("abort", this.#abortListener);
272
+ }
273
+ for (const release of this.#releases)
274
+ release();
275
+ this.#releases.clear();
276
+ }
277
+ #assertActive() {
278
+ if (this.#disposed) {
279
+ throw new DOMException("Resource binding scope has been disposed", "AbortError");
280
+ }
281
+ }
282
+ }
283
+ function validateTable(table, options) {
284
+ const validation = validateResourceBindings(table.bindings, options.requirements ?? [], options.validation ?? {});
285
+ if (!validation.valid) {
286
+ throw new ResourceBindingValidationError(validation.issues);
287
+ }
288
+ return table;
289
+ }
290
+ function validateRequirement(requirement, index, issues) {
291
+ if (!isValidAssetKind(requirement.kind)) {
292
+ issues.push({
293
+ code: "invalid-kind",
294
+ path: ["requirements", index, "kind"],
295
+ message: "Resource requirement kind is invalid",
296
+ ...(isNormalizedAssetRef(requirement.assetRef)
297
+ ? { assetRef: requirement.assetRef }
298
+ : {}),
299
+ });
300
+ }
301
+ if (requirement.mediaType !== undefined) {
302
+ pushInvalidMediaType(requirement.mediaType, ["requirements", index, "mediaType"], requirement.assetRef, issues);
303
+ }
304
+ if (requirement.integrity !== undefined) {
305
+ pushInvalidIntegrity(requirement.integrity, ["requirements", index, "integrity"], requirement.assetRef, issues);
306
+ }
307
+ }
308
+ function validateBinding(binding, index, issues) {
309
+ const assetRef = isNormalizedAssetRef(binding.assetRef)
310
+ ? binding.assetRef
311
+ : undefined;
312
+ const asset = binding.asset;
313
+ if (!asset || typeof asset !== "object") {
314
+ issues.push({
315
+ code: "invalid-asset",
316
+ path: ["bindings", index, "asset"],
317
+ message: "Resource binding asset must be an object",
318
+ ...(assetRef ? { assetRef } : {}),
319
+ });
320
+ return;
321
+ }
322
+ if (!isNormalizedAssetRef(asset.id)) {
323
+ issues.push({
324
+ code: "invalid-asset",
325
+ path: ["bindings", index, "asset", "id"],
326
+ message: "Bound asset id must be normalized",
327
+ ...(assetRef ? { assetRef } : {}),
328
+ });
329
+ }
330
+ if (!isValidAssetKind(asset.kind)) {
331
+ issues.push({
332
+ code: "invalid-kind",
333
+ path: ["bindings", index, "asset", "kind"],
334
+ message: "Bound asset kind is invalid",
335
+ ...(assetRef ? { assetRef } : {}),
336
+ });
337
+ }
338
+ try {
339
+ getAssetResourceLocator(asset);
340
+ }
341
+ catch {
342
+ issues.push({
343
+ code: "missing-source",
344
+ path: ["bindings", index, "asset", "source"],
345
+ message: "Bound asset must provide a resource source",
346
+ ...(assetRef ? { assetRef } : {}),
347
+ });
348
+ }
349
+ if (asset.mediaType !== undefined) {
350
+ pushInvalidMediaType(asset.mediaType, ["bindings", index, "asset", "mediaType"], assetRef, issues);
351
+ }
352
+ if (asset.integrity !== undefined) {
353
+ pushInvalidIntegrity(asset.integrity, ["bindings", index, "asset", "integrity"], assetRef, issues);
354
+ }
355
+ }
356
+ function validateBindingAgainstRequirement(binding, requirement, index, issues) {
357
+ const assetRef = binding.assetRef;
358
+ if (binding.asset.kind !== requirement.kind) {
359
+ issues.push({
360
+ code: "kind-mismatch",
361
+ path: ["bindings", index, "asset", "kind"],
362
+ message: "Bound asset kind does not match the plan requirement",
363
+ assetRef,
364
+ });
365
+ }
366
+ if (requirement.mediaType !== undefined &&
367
+ !mediaTypesEqual(binding.asset.mediaType, requirement.mediaType)) {
368
+ issues.push({
369
+ code: "media-type-mismatch",
370
+ path: ["bindings", index, "asset", "mediaType"],
371
+ message: "Bound asset mediaType does not match the plan requirement",
372
+ assetRef,
373
+ });
374
+ }
375
+ if (requirement.integrity !== undefined &&
376
+ !integritiesEqual(binding.asset.integrity, requirement.integrity)) {
377
+ issues.push({
378
+ code: "integrity-mismatch",
379
+ path: ["bindings", index, "asset", "integrity"],
380
+ message: "Bound asset integrity does not match the plan requirement",
381
+ assetRef,
382
+ });
383
+ }
384
+ }
385
+ function bindingAcquireOptions(binding, options) {
386
+ if (options.expectedMediaType !== undefined) {
387
+ assertMediaType(options.expectedMediaType);
388
+ }
389
+ if (options.expectedKind !== undefined &&
390
+ options.expectedKind !== binding.asset.kind) {
391
+ throw new ResourceBindingValidationError([
392
+ {
393
+ code: "kind-mismatch",
394
+ path: ["assetRef"],
395
+ message: "Requested resource kind does not match the binding",
396
+ assetRef: binding.assetRef,
397
+ },
398
+ ]);
399
+ }
400
+ if (options.expectedMediaType !== undefined &&
401
+ binding.asset.mediaType !== undefined &&
402
+ !mediaTypesEqual(options.expectedMediaType, binding.asset.mediaType)) {
403
+ throw new ResourceBindingValidationError([
404
+ {
405
+ code: "media-type-mismatch",
406
+ path: ["assetRef"],
407
+ message: "Requested mediaType does not match the binding",
408
+ assetRef: binding.assetRef,
409
+ },
410
+ ]);
411
+ }
412
+ return {
413
+ ...options,
414
+ expectedKind: binding.asset.kind,
415
+ ...(binding.asset.mediaType !== undefined
416
+ ? { expectedMediaType: binding.asset.mediaType }
417
+ : {}),
418
+ };
419
+ }
420
+ function assertResolvedResourceMediaType(assetRef, asset, resource) {
421
+ if (asset.mediaType !== undefined &&
422
+ !mediaTypesEqual(resource.mediaType, asset.mediaType)) {
423
+ throw new ResourceBindingValidationError([
424
+ {
425
+ code: "media-type-mismatch",
426
+ path: ["resource", "mediaType"],
427
+ message: "Resolved resource mediaType does not match the asset",
428
+ assetRef,
429
+ },
430
+ ]);
431
+ }
432
+ }
433
+ function assertNormalizedAssetRef(value) {
434
+ if (!isNormalizedAssetRef(value)) {
435
+ throw new Error("Resource reference must be a normalized non-empty string");
436
+ }
437
+ }
438
+ function assertOpaqueResourceRef(value) {
439
+ assertNormalizedAssetRef(value);
440
+ if (value.includes("/") ||
441
+ value.includes("\\") ||
442
+ value.includes("?") ||
443
+ value.includes("#") ||
444
+ /^[a-z][a-z\d+.-]*:\/\//iu.test(value)) {
445
+ throw new Error("Model-visible resourceRef must be opaque");
446
+ }
447
+ }
448
+ function isNormalizedAssetRef(value) {
449
+ return (typeof value === "string" && value.length > 0 && value === value.trim());
450
+ }
451
+ function assertAssetKind(value) {
452
+ if (!isValidAssetKind(value))
453
+ throw new Error("Resource kind is invalid");
454
+ }
455
+ function isValidAssetKind(value) {
456
+ return (value === "model" ||
457
+ value === "texture" ||
458
+ value === "environment" ||
459
+ value === "audio" ||
460
+ value === "data");
461
+ }
462
+ function assertMediaType(value) {
463
+ if (!isValidMediaType(value)) {
464
+ throw new Error("Resource mediaType must be a valid MIME type");
465
+ }
466
+ }
467
+ function isValidMediaType(value) {
468
+ if (typeof value !== "string" ||
469
+ !value.trim() ||
470
+ /[\u0000-\u001f\u007f]/u.test(value)) {
471
+ return false;
472
+ }
473
+ return /^[^\s/;]+\/[^\s/;]+(?:\s*;\s*[^;\r\n]+)*$/u.test(value.trim());
474
+ }
475
+ function pushInvalidMediaType(value, path, assetRef, issues) {
476
+ if (!isValidMediaType(value)) {
477
+ issues.push({
478
+ code: "invalid-media-type",
479
+ path: [...path],
480
+ message: "Resource mediaType must be a valid MIME type",
481
+ ...(isNormalizedAssetRef(assetRef) ? { assetRef } : {}),
482
+ });
483
+ }
484
+ }
485
+ function assertIntegrity(value) {
486
+ if (!isValidIntegrity(value)) {
487
+ throw new Error("Resource integrity must be a lowercase hexadecimal SHA-256 digest");
488
+ }
489
+ }
490
+ function isValidIntegrity(value) {
491
+ return Boolean(value &&
492
+ typeof value === "object" &&
493
+ value.algorithm === "sha256" &&
494
+ typeof value.value === "string" &&
495
+ /^[a-f\d]{64}$/u.test(value.value));
496
+ }
497
+ function pushInvalidIntegrity(value, path, assetRef, issues) {
498
+ if (!isValidIntegrity(value)) {
499
+ issues.push({
500
+ code: "invalid-integrity",
501
+ path: [...path],
502
+ message: "Resource integrity must be a lowercase hexadecimal SHA-256 digest",
503
+ ...(isNormalizedAssetRef(assetRef) ? { assetRef } : {}),
504
+ });
505
+ }
506
+ }
507
+ function mediaTypeForComparison(value) {
508
+ if (value === undefined || value === null)
509
+ return null;
510
+ return isValidMediaType(value)
511
+ ? (value.trim().split(";", 1)[0] ?? "").toLowerCase()
512
+ : null;
513
+ }
514
+ function mediaTypesEqual(first, second) {
515
+ const left = mediaTypeForComparison(first);
516
+ const right = mediaTypeForComparison(second);
517
+ return left !== null && right !== null && left === right;
518
+ }
519
+ function integritiesEqual(first, second) {
520
+ return Boolean(first &&
521
+ second &&
522
+ first.algorithm === second.algorithm &&
523
+ first.value === second.value);
524
+ }
525
+ function ownIssue(issue) {
526
+ return Object.freeze({
527
+ ...issue,
528
+ path: Object.freeze([...issue.path]),
529
+ });
530
+ }
531
+ function formatIssue(issue) {
532
+ const path = issue.path.reduce((result, segment) => typeof segment === "number"
533
+ ? `${result}[${segment}]`
534
+ : `${result}.${segment}`, "$");
535
+ return `${path}: ${issue.message}`;
536
+ }
537
+ function freezeValue(value) {
538
+ if (!value || typeof value !== "object" || Object.isFrozen(value)) {
539
+ return value;
540
+ }
541
+ for (const child of Object.values(value)) {
542
+ freezeValue(child);
543
+ }
544
+ return Object.freeze(value);
545
+ }
546
+ //# sourceMappingURL=resource-bindings.js.map