@forgeax/engine-import 0.1.23 → 0.1.25

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 (30) hide show
  1. package/README.md +5 -5
  2. package/dist/__tests__/scriptable-pack-build.unit.test.d.ts +2 -0
  3. package/dist/__tests__/scriptable-pack-build.unit.test.d.ts.map +1 -0
  4. package/dist/build-production.d.ts.map +1 -1
  5. package/dist/index.d.ts +4 -4
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.mjs +284 -60
  8. package/dist/index.mjs.map +1 -1
  9. package/dist/{parameterized-scriptable-pack.d.ts → scriptable-pack-build.d.ts} +20 -15
  10. package/dist/scriptable-pack-build.d.ts.map +1 -0
  11. package/dist/scriptable-pack-host.d.ts +15 -13
  12. package/dist/scriptable-pack-host.d.ts.map +1 -1
  13. package/dist/scriptable-pack.d.ts +2 -1
  14. package/dist/scriptable-pack.d.ts.map +1 -1
  15. package/dist/source-package-publication.d.ts +7 -0
  16. package/dist/source-package-publication.d.ts.map +1 -1
  17. package/package.json +8 -8
  18. package/src/__tests__/{parameterized-scriptable-pack.unit.test.ts → scriptable-pack-build.unit.test.ts} +11 -14
  19. package/src/__tests__/scriptable-pack-host.unit.test.ts +53 -5
  20. package/src/__tests__/scriptable-pack-output-producers.unit.test.ts +4 -5
  21. package/src/__tests__/source-package-publication.integration.test.ts +141 -0
  22. package/src/build-production.ts +45 -50
  23. package/src/index.ts +17 -16
  24. package/src/{parameterized-scriptable-pack.ts → scriptable-pack-build.ts} +91 -27
  25. package/src/scriptable-pack-host.ts +48 -40
  26. package/src/scriptable-pack.ts +2 -1
  27. package/src/source-package-publication.ts +242 -10
  28. package/dist/__tests__/parameterized-scriptable-pack.unit.test.d.ts +0 -2
  29. package/dist/__tests__/parameterized-scriptable-pack.unit.test.d.ts.map +0 -1
  30. package/dist/parameterized-scriptable-pack.d.ts.map +0 -1
@@ -1,7 +1,8 @@
1
- import { randomUUID } from 'node:crypto';
1
+ import { createHash, randomUUID } from 'node:crypto';
2
2
  import { mkdir, rename, rm, stat, writeFile } from 'node:fs/promises';
3
3
  import { dirname } from 'node:path';
4
4
  import {
5
+ type DdcArtifact,
5
6
  type DdcEntry,
6
7
  type DdcGenerationEntryCandidate,
7
8
  DdcGenerationSession,
@@ -9,6 +10,7 @@ import {
9
10
  DdcLifecycle,
10
11
  ddcOutputDigest,
11
12
  } from '@forgeax/engine-ddc';
13
+ import { canonicalDdcJson } from '@forgeax/engine-ddc/key';
12
14
  import {
13
15
  type CatalogEntry,
14
16
  err,
@@ -45,9 +47,17 @@ export interface ImportPublicationInput {
45
47
  readonly transport?: {
46
48
  readonly path: string;
47
49
  readonly body: string;
50
+ /** Complete package-relative artifact closure for the published Pack. */
51
+ readonly artifacts?: readonly ImportPublicationArtifact[];
48
52
  };
49
53
  }
50
54
 
55
+ export interface ImportPublicationArtifact {
56
+ readonly path: string;
57
+ readonly mediaType: string;
58
+ readonly bytes: Uint8Array;
59
+ }
60
+
51
61
  export interface ImportPublicationError {
52
62
  readonly code: SourcePackageError['code'];
53
63
  readonly expected: string;
@@ -184,6 +194,225 @@ function importPublicationFailure(error: SourcePackageError): ImportPublicationE
184
194
  };
185
195
  }
186
196
 
197
+ function publicationArtifacts(
198
+ transport: ImportPublicationInput['transport'],
199
+ ): Readonly<Record<string, DdcArtifact>> {
200
+ return Object.fromEntries(
201
+ (transport?.artifacts ?? []).map((artifact) => [
202
+ artifact.path,
203
+ { mediaType: artifact.mediaType, bytes: artifact.bytes },
204
+ ]),
205
+ );
206
+ }
207
+
208
+ function publicationContext(input: ImportPublicationInput): SourcePackageErrorContext {
209
+ return {
210
+ sourceMeta: '<import-publication>',
211
+ anchorGuid: input.guid,
212
+ affectedGuids: input.publishedGuids,
213
+ producer: 'source-package/import-publication',
214
+ importer: 'import-publication',
215
+ };
216
+ }
217
+
218
+ /**
219
+ * Validate the Pack-to-transport closure before DDC staging can advance its
220
+ * head. The Pack is the authority for required artifact paths; a missing or
221
+ * mismatched body must never become a published `current` generation.
222
+ */
223
+ function validatePublicationArtifactClosure(
224
+ input: ImportPublicationInput,
225
+ ): Result<Readonly<Record<string, DdcArtifact>>, SourcePackageError> {
226
+ const context = publicationContext(input);
227
+ const pack = input.pack;
228
+ if (
229
+ pack === null ||
230
+ typeof pack !== 'object' ||
231
+ (pack as { readonly schemaVersion?: unknown }).schemaVersion !== '2.0.0' ||
232
+ (pack as { readonly kind?: unknown }).kind !== 'internal-text-package'
233
+ ) {
234
+ return ok(publicationArtifacts(input.transport));
235
+ }
236
+ const assets = (pack as { readonly assets?: unknown }).assets;
237
+ if (!Array.isArray(assets)) {
238
+ return err(
239
+ sourcePackageError('source-package-publication-invalid', context, {
240
+ stage: 'route-integrity',
241
+ reason: 'published Pack does not contain an assets array',
242
+ }),
243
+ );
244
+ }
245
+
246
+ const required = new Map<
247
+ string,
248
+ {
249
+ readonly mediaType?: string;
250
+ readonly byteLength?: number;
251
+ readonly integrity?: { readonly algorithm: string; readonly digest: string };
252
+ }
253
+ >();
254
+ for (const asset of assets) {
255
+ if (asset === null || typeof asset !== 'object') {
256
+ return err(
257
+ sourcePackageError('source-package-publication-invalid', context, {
258
+ stage: 'route-integrity',
259
+ reason: 'published Pack contains a non-object asset row',
260
+ }),
261
+ );
262
+ }
263
+ const rawArtifacts = (asset as { readonly artifacts?: unknown }).artifacts;
264
+ if (rawArtifacts === undefined) continue;
265
+ if (rawArtifacts === null || typeof rawArtifacts !== 'object' || Array.isArray(rawArtifacts)) {
266
+ return err(
267
+ sourcePackageError('source-package-publication-invalid', context, {
268
+ stage: 'route-integrity',
269
+ reason: 'published Pack contains an invalid asset artifact map',
270
+ }),
271
+ );
272
+ }
273
+ for (const [localKey, rawDescriptor] of Object.entries(
274
+ rawArtifacts as Record<string, unknown>,
275
+ )) {
276
+ if (rawDescriptor === null || typeof rawDescriptor !== 'object') {
277
+ return err(
278
+ sourcePackageError('source-package-publication-invalid', context, {
279
+ stage: 'route-integrity',
280
+ reason: `artifact descriptor ${localKey} is not an object`,
281
+ }),
282
+ );
283
+ }
284
+ const descriptor = rawDescriptor as Record<string, unknown>;
285
+ const path = descriptor.path;
286
+ if (typeof path !== 'string' || path.length === 0) {
287
+ return err(
288
+ sourcePackageError('source-package-publication-invalid', context, {
289
+ stage: 'route-integrity',
290
+ reason: `artifact descriptor ${localKey} has no package-relative path`,
291
+ }),
292
+ );
293
+ }
294
+ const mediaType = descriptor.mediaType;
295
+ const byteLength = descriptor.byteLength;
296
+ const integrityValue = descriptor.integrity;
297
+ const integrity =
298
+ integrityValue !== null && typeof integrityValue === 'object'
299
+ ? {
300
+ algorithm: (integrityValue as { readonly algorithm?: unknown }).algorithm,
301
+ digest: (integrityValue as { readonly digest?: unknown }).digest,
302
+ }
303
+ : undefined;
304
+ if (
305
+ (mediaType !== undefined && typeof mediaType !== 'string') ||
306
+ (byteLength !== undefined &&
307
+ (!Number.isSafeInteger(byteLength) || (byteLength as number) < 0)) ||
308
+ (integrityValue !== undefined &&
309
+ (integrity === undefined ||
310
+ typeof integrity.algorithm !== 'string' ||
311
+ typeof integrity.digest !== 'string'))
312
+ ) {
313
+ return err(
314
+ sourcePackageError('source-package-publication-invalid', context, {
315
+ stage: 'route-integrity',
316
+ reason: `artifact descriptor ${path} has invalid metadata`,
317
+ }),
318
+ );
319
+ }
320
+ if (required.has(path)) {
321
+ return err(
322
+ sourcePackageError('source-package-publication-invalid', context, {
323
+ stage: 'route-integrity',
324
+ reason: `artifact path ${path} is declared more than once`,
325
+ }),
326
+ );
327
+ }
328
+ required.set(path, {
329
+ ...(typeof mediaType === 'string' ? { mediaType } : {}),
330
+ ...(typeof byteLength === 'number' ? { byteLength } : {}),
331
+ ...(integrity !== undefined &&
332
+ typeof integrity.algorithm === 'string' &&
333
+ typeof integrity.digest === 'string'
334
+ ? { integrity: { algorithm: integrity.algorithm, digest: integrity.digest } }
335
+ : {}),
336
+ });
337
+ }
338
+ }
339
+
340
+ const available = new Map<string, ImportPublicationArtifact>();
341
+ const duplicatePaths: string[] = [];
342
+ for (const artifact of input.transport?.artifacts ?? []) {
343
+ if (available.has(artifact.path)) duplicatePaths.push(artifact.path);
344
+ available.set(artifact.path, artifact);
345
+ }
346
+ const missing: string[] = [];
347
+ const mismatched: string[] = [...duplicatePaths.map((path) => `${path}: duplicate body`)];
348
+ for (const path of available.keys()) {
349
+ if (!required.has(path)) mismatched.push(`${path}: unexpected body`);
350
+ }
351
+ for (const [path, descriptor] of required) {
352
+ const artifact = available.get(path);
353
+ if (artifact === undefined) {
354
+ missing.push(path);
355
+ continue;
356
+ }
357
+ if (!(artifact.bytes instanceof Uint8Array)) {
358
+ mismatched.push(`${path}: body is not Uint8Array`);
359
+ continue;
360
+ }
361
+ if (descriptor.mediaType !== undefined && artifact.mediaType !== descriptor.mediaType) {
362
+ mismatched.push(`${path}: media type mismatch`);
363
+ }
364
+ if (
365
+ descriptor.byteLength !== undefined &&
366
+ artifact.bytes.byteLength !== descriptor.byteLength
367
+ ) {
368
+ mismatched.push(`${path}: byte length mismatch`);
369
+ }
370
+ if (descriptor.integrity !== undefined) {
371
+ const actualDigest = `sha256:${createHash('sha256').update(artifact.bytes).digest('hex')}`;
372
+ if (
373
+ descriptor.integrity.algorithm !== 'sha256' ||
374
+ descriptor.integrity.digest !== actualDigest
375
+ ) {
376
+ mismatched.push(`${path}: integrity mismatch`);
377
+ }
378
+ }
379
+ }
380
+ if (missing.length > 0 || mismatched.length > 0) {
381
+ return err(
382
+ sourcePackageError('source-package-publication-invalid', context, {
383
+ stage: 'route-integrity',
384
+ reason: 'Pack artifact closure is incomplete or mismatched',
385
+ ...(missing.length === 0 ? {} : { missing }),
386
+ ...(mismatched.length === 0 ? {} : { unexpected: mismatched }),
387
+ }),
388
+ );
389
+ }
390
+
391
+ if (input.transport !== undefined) {
392
+ let transportedPack: unknown;
393
+ try {
394
+ transportedPack = JSON.parse(input.transport.body) as unknown;
395
+ } catch {
396
+ return err(
397
+ sourcePackageError('source-package-publication-invalid', context, {
398
+ stage: 'route-integrity',
399
+ reason: 'transport body is not valid JSON for the published Pack',
400
+ }),
401
+ );
402
+ }
403
+ if (canonicalDdcJson(transportedPack) !== canonicalDdcJson(pack)) {
404
+ return err(
405
+ sourcePackageError('source-package-publication-invalid', context, {
406
+ stage: 'route-integrity',
407
+ reason: 'transport body does not match the published Pack',
408
+ }),
409
+ );
410
+ }
411
+ }
412
+
413
+ return ok(publicationArtifacts(input.transport));
414
+ }
415
+
187
416
  function projectImportPublication(
188
417
  input: ImportPublicationInput,
189
418
  head: DdcHead,
@@ -231,6 +460,15 @@ export async function publishImportPublication(
231
460
  export async function stageImportPublication(
232
461
  input: ImportPublicationInput,
233
462
  ): Promise<StagedImportPublicationResult> {
463
+ const validatedArtifacts = validatePublicationArtifactClosure(input);
464
+ if (!validatedArtifacts.ok) {
465
+ return {
466
+ ok: false,
467
+ error: importPublicationFailure(validatedArtifacts.error),
468
+ head: await inspectHead(input.root, input.guid, input.desiredKey),
469
+ };
470
+ }
471
+ const artifacts = validatedArtifacts.value;
234
472
  const staged = await stageSourcePackageDdc({
235
473
  root: input.root,
236
474
  entry: {
@@ -238,7 +476,7 @@ export async function stageImportPublication(
238
476
  guid: input.guid,
239
477
  payload: input.pack,
240
478
  refs: [],
241
- artifacts: {},
479
+ artifacts,
242
480
  receipt: {
243
481
  guid: input.guid,
244
482
  key: input.desiredKey,
@@ -248,17 +486,11 @@ export async function stageImportPublication(
248
486
  guid: input.guid,
249
487
  payload: input.pack,
250
488
  refs: [],
251
- artifacts: {},
489
+ artifacts,
252
490
  }),
253
491
  },
254
492
  },
255
- context: {
256
- sourceMeta: '<import-publication>',
257
- anchorGuid: input.guid,
258
- affectedGuids: input.publishedGuids,
259
- producer: 'source-package/import-publication',
260
- importer: 'import-publication',
261
- },
493
+ context: publicationContext(input),
262
494
  });
263
495
  if (!staged.ok) {
264
496
  return {
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=parameterized-scriptable-pack.unit.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"parameterized-scriptable-pack.unit.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/parameterized-scriptable-pack.unit.test.ts"],"names":[],"mappings":""}
@@ -1 +0,0 @@
1
- {"version":3,"file":"parameterized-scriptable-pack.d.ts","sourceRoot":"","sources":["../src/parameterized-scriptable-pack.ts"],"names":[],"mappings":"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"}