@bitmagic/asset-core 0.1.1 → 0.1.3

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.
@@ -0,0 +1,74 @@
1
+ /**
2
+ * The published game's browser-tab icon: the one `<link>` element, the one storage-key
3
+ * derivation, and the one function that swaps the default out for a game's own.
4
+ *
5
+ * Import path matters: this module is published as the `@bitmagic/asset-core/favicon`
6
+ * subpath so its consumers do not drag in the root barrel's zod / three / forger client.
7
+ * It is node-only (`node:crypto`) — the three consumers are game-play-agent (web publish
8
+ * lane), api-server (CLI publish lane) and the CLI itself. The Creator never imports it;
9
+ * it uploads a source image and lets publish do the deriving.
10
+ *
11
+ * Why this lives in a shared package rather than being duplicated per lane: the two
12
+ * publish lanes must agree on the marker id and on the exact `<link>` they emit, or a
13
+ * game's icon silently reverts to the Bitmagic mark in whichever lane drifted. The pieces
14
+ * where drift breaks correctness are shared here; the sharp resize is NOT (it would drag
15
+ * a heavy native dep into a package the browser-facing subpaths live beside), and is
16
+ * deliberately mirrored in each lane instead.
17
+ */
18
+ /**
19
+ * The id on the default `<link rel="icon">` baked into every game bundle — `game/index.html`
20
+ * for the web lane, `renderIndexHtml()` for CLI projects. Publish finds the element by this
21
+ * id and replaces it wholesale.
22
+ *
23
+ * Replacing rather than appending a second link is deliberate. Two `sizes`-less `rel="icon"`
24
+ * links is ambiguous input: "the last one wins" is browser convention, not spec, and Safari
25
+ * has historically picked differently from Chrome. After `applyFaviconLink` the document
26
+ * carries exactly one icon link, which is something a test can assert.
27
+ */
28
+ export declare const FAVICON_LINK_ID = "bm-favicon";
29
+ /**
30
+ * Edge length of the derived icon. A retina browser tab renders a 32 CSS px icon at 2x, so
31
+ * 64 is the real ceiling that matters; it also covers bookmark and taskbar use at 48.
32
+ * Browsers downscale cleanly, so a single size beats emitting several `sizes=` variants.
33
+ */
34
+ export declare const FAVICON_PX = 64;
35
+ /**
36
+ * 32 hex chars of sha256 over the SOURCE URL — not over the source bytes.
37
+ *
38
+ * Hashing the URL is what makes the cache free: the storage key is computable without
39
+ * fetching anything, so a cache hit costs one existence probe and no download. It is sound
40
+ * because every cover/upload URL in both lanes is already timestamp-unique per upload, so
41
+ * new art always produces a new URL and therefore a new key.
42
+ */
43
+ export declare function faviconContentHash(sourceUrl: string): string;
44
+ /**
45
+ * Where the web lane stores an icon derived from `sourceUrl`, relative to the storage
46
+ * prefix (`worlds/v3`). The CLI lane composes its own game-scoped key from
47
+ * `faviconContentHash` instead, because its bucket layout is per-game rather than shared.
48
+ *
49
+ * The `v1` segment and the `-64` suffix are load-bearing: changing the crop or the size
50
+ * must produce a NEW key rather than silently serving the old bytes from an immutable
51
+ * `max-age=31536000` object. Same reasoning as the hand-rolled `-v2` on the watermark logo.
52
+ */
53
+ export declare function faviconStorageKey(sourceUrl: string): string;
54
+ /** The exact element every lane emits. One definition so the lanes cannot disagree. */
55
+ export declare function renderFaviconLink(href: string): string;
56
+ /**
57
+ * Point the document's icon at `href`.
58
+ *
59
+ * Replaces the element carrying `id="bm-favicon"`. When that marker is absent — an older
60
+ * cached bundle, or a CLI project that has not run `bitmagic upgrade` since the marker
61
+ * shipped — falls back to inserting before `</head>`, which is best-effort but never worse
62
+ * than the un-marked status quo.
63
+ *
64
+ * Returns `html` unchanged when `href` is missing or not an allowed scheme, and never
65
+ * throws: a tab icon must not be able to fail a publish.
66
+ *
67
+ * `indexOf` rather than a regex on purpose. The default href is a ~700-char base64 data URI
68
+ * sitting inside a bundle that reaches 20 MB, and the surrounding code (`injectIntoHtml`)
69
+ * is string-insertion for the same reason. Anchored `indexOf` scans are O(n) with tiny
70
+ * constants and have obvious failure semantics; a backtracking regex over that string does
71
+ * not.
72
+ */
73
+ export declare function applyFaviconLink(html: string, href: string | null | undefined): string;
74
+ //# sourceMappingURL=favicon.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"favicon.d.ts","sourceRoot":"","sources":["../src/favicon.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAIH;;;;;;;;;GASG;AACH,eAAO,MAAM,eAAe,eAAe,CAAC;AAE5C;;;;GAIG;AACH,eAAO,MAAM,UAAU,KAAK,CAAC;AAqB7B;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAE5D;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAE3D;AAED,uFAAuF;AACvF,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEtD;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAuBtF"}
@@ -0,0 +1,130 @@
1
+ /**
2
+ * The published game's browser-tab icon: the one `<link>` element, the one storage-key
3
+ * derivation, and the one function that swaps the default out for a game's own.
4
+ *
5
+ * Import path matters: this module is published as the `@bitmagic/asset-core/favicon`
6
+ * subpath so its consumers do not drag in the root barrel's zod / three / forger client.
7
+ * It is node-only (`node:crypto`) — the three consumers are game-play-agent (web publish
8
+ * lane), api-server (CLI publish lane) and the CLI itself. The Creator never imports it;
9
+ * it uploads a source image and lets publish do the deriving.
10
+ *
11
+ * Why this lives in a shared package rather than being duplicated per lane: the two
12
+ * publish lanes must agree on the marker id and on the exact `<link>` they emit, or a
13
+ * game's icon silently reverts to the Bitmagic mark in whichever lane drifted. The pieces
14
+ * where drift breaks correctness are shared here; the sharp resize is NOT (it would drag
15
+ * a heavy native dep into a package the browser-facing subpaths live beside), and is
16
+ * deliberately mirrored in each lane instead.
17
+ */
18
+ import { createHash } from 'node:crypto';
19
+ /**
20
+ * The id on the default `<link rel="icon">` baked into every game bundle — `game/index.html`
21
+ * for the web lane, `renderIndexHtml()` for CLI projects. Publish finds the element by this
22
+ * id and replaces it wholesale.
23
+ *
24
+ * Replacing rather than appending a second link is deliberate. Two `sizes`-less `rel="icon"`
25
+ * links is ambiguous input: "the last one wins" is browser convention, not spec, and Safari
26
+ * has historically picked differently from Chrome. After `applyFaviconLink` the document
27
+ * carries exactly one icon link, which is something a test can assert.
28
+ */
29
+ export const FAVICON_LINK_ID = 'bm-favicon';
30
+ /**
31
+ * Edge length of the derived icon. A retina browser tab renders a 32 CSS px icon at 2x, so
32
+ * 64 is the real ceiling that matters; it also covers bookmark and taskbar use at 48.
33
+ * Browsers downscale cleanly, so a single size beats emitting several `sizes=` variants.
34
+ */
35
+ export const FAVICON_PX = 64;
36
+ /** Attribute-safe. The href reaches a public page's `<head>` and may be creator-supplied. */
37
+ function escapeAttr(value) {
38
+ return value
39
+ .replace(/&/g, '&amp;')
40
+ .replace(/"/g, '&quot;')
41
+ .replace(/</g, '&lt;')
42
+ .replace(/>/g, '&gt;');
43
+ }
44
+ /**
45
+ * Only these two forms may become an icon href. Escaping alone is not the whole guard — a
46
+ * `javascript:` URL in a `<link href>` is inert in modern browsers, but this string is
47
+ * assembled from creator-controlled data and baked into a public page, so the allowlist is
48
+ * the guard and the escaping is defence in depth.
49
+ */
50
+ function isAllowedHref(href) {
51
+ return /^https:\/\//i.test(href) || /^data:image\//i.test(href);
52
+ }
53
+ /**
54
+ * 32 hex chars of sha256 over the SOURCE URL — not over the source bytes.
55
+ *
56
+ * Hashing the URL is what makes the cache free: the storage key is computable without
57
+ * fetching anything, so a cache hit costs one existence probe and no download. It is sound
58
+ * because every cover/upload URL in both lanes is already timestamp-unique per upload, so
59
+ * new art always produces a new URL and therefore a new key.
60
+ */
61
+ export function faviconContentHash(sourceUrl) {
62
+ return createHash('sha256').update(sourceUrl).digest('hex').slice(0, 32);
63
+ }
64
+ /**
65
+ * Where the web lane stores an icon derived from `sourceUrl`, relative to the storage
66
+ * prefix (`worlds/v3`). The CLI lane composes its own game-scoped key from
67
+ * `faviconContentHash` instead, because its bucket layout is per-game rather than shared.
68
+ *
69
+ * The `v1` segment and the `-64` suffix are load-bearing: changing the crop or the size
70
+ * must produce a NEW key rather than silently serving the old bytes from an immutable
71
+ * `max-age=31536000` object. Same reasoning as the hand-rolled `-v2` on the watermark logo.
72
+ */
73
+ export function faviconStorageKey(sourceUrl) {
74
+ return `assets/favicons/v1/${faviconContentHash(sourceUrl)}-${FAVICON_PX}.png`;
75
+ }
76
+ /** The exact element every lane emits. One definition so the lanes cannot disagree. */
77
+ export function renderFaviconLink(href) {
78
+ return `<link id="${FAVICON_LINK_ID}" rel="icon" type="image/png" href="${escapeAttr(href)}">`;
79
+ }
80
+ /**
81
+ * Point the document's icon at `href`.
82
+ *
83
+ * Replaces the element carrying `id="bm-favicon"`. When that marker is absent — an older
84
+ * cached bundle, or a CLI project that has not run `bitmagic upgrade` since the marker
85
+ * shipped — falls back to inserting before `</head>`, which is best-effort but never worse
86
+ * than the un-marked status quo.
87
+ *
88
+ * Returns `html` unchanged when `href` is missing or not an allowed scheme, and never
89
+ * throws: a tab icon must not be able to fail a publish.
90
+ *
91
+ * `indexOf` rather than a regex on purpose. The default href is a ~700-char base64 data URI
92
+ * sitting inside a bundle that reaches 20 MB, and the surrounding code (`injectIntoHtml`)
93
+ * is string-insertion for the same reason. Anchored `indexOf` scans are O(n) with tiny
94
+ * constants and have obvious failure semantics; a backtracking regex over that string does
95
+ * not.
96
+ */
97
+ export function applyFaviconLink(html, href) {
98
+ if (!href || !isAllowedHref(href))
99
+ return html;
100
+ const link = renderFaviconLink(href);
101
+ const needle = `id="${FAVICON_LINK_ID}"`;
102
+ for (let at = html.indexOf(needle); at !== -1; at = html.indexOf(needle, at + needle.length)) {
103
+ const open = html.lastIndexOf('<', at);
104
+ const close = html.indexOf('>', at);
105
+ // A marker with no enclosing element is malformed input, not something to guess at.
106
+ if (open === -1 || close === -1)
107
+ continue;
108
+ // It has to be an actual <link> element. The id is also NAMED in prose — the explanatory
109
+ // comment above the default link in game/index.html, this doc block, docs — and matching the
110
+ // first occurrence anywhere replaced a comment instead of the link, which left the real
111
+ // default in place and shipped a page with two icon links. Verified the hard way.
112
+ if (!/^<link[\s/>]/i.test(html.slice(open, open + 6)))
113
+ continue;
114
+ if (isInsideComment(html, open))
115
+ continue;
116
+ return html.slice(0, open) + link + html.slice(close + 1);
117
+ }
118
+ const headClose = html.indexOf('</head>');
119
+ if (headClose === -1)
120
+ return html;
121
+ return html.slice(0, headClose) + link + '\n' + html.slice(headClose);
122
+ }
123
+ /** Is `at` inside an HTML comment? True when the nearest `<!--` behind it is not yet closed. */
124
+ function isInsideComment(html, at) {
125
+ const open = html.lastIndexOf('<!--', at);
126
+ if (open === -1)
127
+ return false;
128
+ return html.lastIndexOf('-->', at) < open;
129
+ }
130
+ //# sourceMappingURL=favicon.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"favicon.js","sourceRoot":"","sources":["../src/favicon.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,43 @@
1
+ import { z } from 'zod';
2
+ import type { AssetResultBase, GenerateDeps } from '../types.js';
3
+ /**
4
+ * A 3D prop mesh, from a prompt.
5
+ *
6
+ * The odd one out among the generators: it returns NO world.json patches. Every other generator
7
+ * produces a finished asset the host can write straight away, but a prop is only half made when
8
+ * the Asset Forger hands back a GLB — the engine still has to voxelize it, and voxelizing happens
9
+ * in a browser with the game loaded, which is the caller's side of the seam in both lanes. So this
10
+ * returns the GLB URL and stops, and the host decides what to do with it.
11
+ *
12
+ * That is what makes the same code serve the CLI's `bitmagic generate prop` (voxelize in a
13
+ * headless Chrome, upsert the asset locally) and, in principle, the web lane's HQ job — which
14
+ * today reimplements this call inline in `hq-asset-jobs.ts`.
15
+ *
16
+ * The generation is billable the moment the Forger accepts the job: the vendor charges on
17
+ * acceptance, so a downstream failure to produce a URL still costs.
18
+ */
19
+ export declare const propParamsSchema: z.ZodObject<{
20
+ prompt: z.ZodString;
21
+ /**
22
+ * `prop` matches what the HQ path has always sent. Named rather than hardcoded because the
23
+ * Forger distinguishes props from characters, and a mis-typed model produces a rigged humanoid
24
+ * where a rock was wanted.
25
+ */
26
+ modelType: z.ZodDefault<z.ZodOptional<z.ZodEnum<["prop"]>>>;
27
+ }, "strict", z.ZodTypeAny, {
28
+ prompt: string;
29
+ modelType: "prop";
30
+ }, {
31
+ prompt: string;
32
+ modelType?: "prop" | undefined;
33
+ }>;
34
+ export type PropParams = z.input<typeof propParamsSchema>;
35
+ export type PropResult = AssetResultBase & {
36
+ /** Where the generated mesh landed. The host voxelizes this. */
37
+ glbUrl?: string;
38
+ billable?: boolean;
39
+ };
40
+ export declare function generateProp(params: PropParams, deps: GenerateDeps & {
41
+ imageToMeshModel?: string;
42
+ }): Promise<PropResult>;
43
+ //# sourceMappingURL=prop.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prop.d.ts","sourceRoot":"","sources":["../../src/generators/prop.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAIjE;;;;;;;;;;;;;;;GAeG;AAEH,eAAO,MAAM,gBAAgB;;IAI3B;;;;OAIG;;;;;;;;EAEM,CAAC;AAEZ,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAE1D,MAAM,MAAM,UAAU,GAAG,eAAe,GAAG;IACzC,gEAAgE;IAChE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,CAAC;AAcF,wBAAsB,YAAY,CAChC,MAAM,EAAE,UAAU,EAClB,IAAI,EAAE,YAAY,GAAG;IAAE,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAAE,GACjD,OAAO,CAAC,UAAU,CAAC,CAsDrB"}
@@ -0,0 +1,80 @@
1
+ import { z } from 'zod';
2
+ import { fail, ok } from '../result.js';
3
+ import { formatZodIssues } from '../validation.js';
4
+ /**
5
+ * A 3D prop mesh, from a prompt.
6
+ *
7
+ * The odd one out among the generators: it returns NO world.json patches. Every other generator
8
+ * produces a finished asset the host can write straight away, but a prop is only half made when
9
+ * the Asset Forger hands back a GLB — the engine still has to voxelize it, and voxelizing happens
10
+ * in a browser with the game loaded, which is the caller's side of the seam in both lanes. So this
11
+ * returns the GLB URL and stops, and the host decides what to do with it.
12
+ *
13
+ * That is what makes the same code serve the CLI's `bitmagic generate prop` (voxelize in a
14
+ * headless Chrome, upsert the asset locally) and, in principle, the web lane's HQ job — which
15
+ * today reimplements this call inline in `hq-asset-jobs.ts`.
16
+ *
17
+ * The generation is billable the moment the Forger accepts the job: the vendor charges on
18
+ * acceptance, so a downstream failure to produce a URL still costs.
19
+ */
20
+ export const propParamsSchema = z.object({
21
+ prompt: z.string()
22
+ .min(1)
23
+ .describe('What the object should be (e.g. "a weathered stone archway covered in moss")'),
24
+ /**
25
+ * `prop` matches what the HQ path has always sent. Named rather than hardcoded because the
26
+ * Forger distinguishes props from characters, and a mis-typed model produces a rigged humanoid
27
+ * where a rock was wanted.
28
+ */
29
+ modelType: z.enum(['prop']).optional().default('prop'),
30
+ }).strict();
31
+ const ENDPOINT = '/ai/v1/forge3d/jobs';
32
+ /**
33
+ * Kept in step with `hq-asset-jobs.ts`, which is the proven configuration for this endpoint.
34
+ * `rigging: 'false'` is a STRING because that is what the Forger's API accepts.
35
+ */
36
+ const DEFAULT_IMAGE_TO_MESH_MODEL = 'unified-trellis';
37
+ export async function generateProp(params, deps) {
38
+ const { forger, logger, onProgress } = deps;
39
+ const parsed = propParamsSchema.safeParse(params);
40
+ if (!parsed.success) {
41
+ logger.warn({ err: parsed.error }, '[prop] Invalid parameters');
42
+ return fail(formatZodIssues(parsed.error));
43
+ }
44
+ const input = parsed.data;
45
+ if (!forger.hasCredentials()) {
46
+ logger.error('[prop] Missing Asset Forger credentials');
47
+ return fail(forger.getMissingCredentialsMessage());
48
+ }
49
+ onProgress(`Generating a mesh for: ${input.prompt}`);
50
+ logger.info(`[prop] Starting forge3d for "${input.prompt}"`);
51
+ let response;
52
+ try {
53
+ response = await forger.postJobAndWait(ENDPOINT, {
54
+ model: 'lab',
55
+ prompt: input.prompt,
56
+ parameters: {
57
+ imageToMeshModel: deps.imageToMeshModel ?? DEFAULT_IMAGE_TO_MESH_MODEL,
58
+ modelType: input.modelType,
59
+ rigging: 'false',
60
+ },
61
+ }, 'prop');
62
+ }
63
+ catch (error) {
64
+ const message = error instanceof Error ? error.message : 'Unknown error occurred';
65
+ logger.error({ err: error }, '[prop] Error during generation');
66
+ return fail(message);
67
+ }
68
+ const glbUrl = response.data?.fileUrl;
69
+ if (!glbUrl) {
70
+ // Billable: the Forger accepted and ran the job, and was paid for it, whatever it answered.
71
+ return { ...fail('Asset Forger returned no file URL for the generated mesh'), billable: true };
72
+ }
73
+ logger.info(`[prop] Generated ${glbUrl}`);
74
+ return ok(`Mesh generated: ${glbUrl}`,
75
+ // Deliberately empty — see the file header. The asset does not exist until the host has
76
+ // voxelized this, and writing a half-made entry would leave world.json describing an asset the
77
+ // engine cannot load.
78
+ [], { glbUrl, billable: true });
79
+ }
80
+ //# sourceMappingURL=prop.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prop.js","sourceRoot":"","sources":["../../src/generators/prop.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -11,6 +11,8 @@ export { generateImageAsset, imageAssetParamsSchema } from './generators/image.j
11
11
  export type { ImageAssetParams, ImageAssetResult } from './generators/image.js';
12
12
  export { generateCharacter, characterParamsSchema } from './generators/character.js';
13
13
  export type { CharacterParams, CharacterResult } from './generators/character.js';
14
+ export { generateProp, propParamsSchema } from './generators/prop.js';
15
+ export type { PropParams, PropResult } from './generators/prop.js';
14
16
  export { generateAnimationAsset, animationParamsSchema, getConfiguredAnimationModel, ANIMATION_MODEL_TEXT_TO_MOTION_3, ANIMATION_MODEL_TEXT_TO_MOTION_2, ANIMATION_MODEL_OLDER, } from './generators/animation.js';
15
17
  export type { AnimationParams, AnimationResult } from './generators/animation.js';
16
18
  export { fetchContentLength } from './http.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,eAAe,EACf,UAAU,EACV,cAAc,EACd,YAAY,EACZ,YAAY,EACZ,eAAe,EACf,UAAU,GACX,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAC/F,OAAO,EAAE,IAAI,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AACvC,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AACvD,YAAY,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAC;AACjE,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAC5E,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AACzE,OAAO,EAAE,mBAAmB,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAC;AAC5F,YAAY,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AACzF,OAAO,EAAE,kBAAkB,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AACnF,YAAY,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAChF,OAAO,EAAE,iBAAiB,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAC;AACrF,YAAY,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAClF,OAAO,EACL,sBAAsB,EACtB,qBAAqB,EACrB,2BAA2B,EAC3B,gCAAgC,EAChC,gCAAgC,EAChC,qBAAqB,GACtB,MAAM,2BAA2B,CAAC;AACnC,YAAY,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAClF,OAAO,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAC/C,OAAO,EAAE,iBAAiB,EAAE,sBAAsB,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAMnG,OAAO,EACL,sBAAsB,EACtB,wBAAwB,EACxB,oBAAoB,EACpB,qBAAqB,EACrB,kBAAkB,EAClB,oBAAoB,GACrB,MAAM,uBAAuB,CAAC;AAC/B,YAAY,EACV,yBAAyB,EACzB,eAAe,EACf,eAAe,EACf,mBAAmB,EACnB,qBAAqB,GACtB,MAAM,uBAAuB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,eAAe,EACf,UAAU,EACV,cAAc,EACd,YAAY,EACZ,YAAY,EACZ,eAAe,EACf,UAAU,GACX,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAC/F,OAAO,EAAE,IAAI,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AACvC,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AACvD,YAAY,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAC;AACjE,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAC5E,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AACzE,OAAO,EAAE,mBAAmB,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAC;AAC5F,YAAY,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AACzF,OAAO,EAAE,kBAAkB,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AACnF,YAAY,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAChF,OAAO,EAAE,iBAAiB,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAC;AACrF,YAAY,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAClF,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AACtE,YAAY,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AACnE,OAAO,EACL,sBAAsB,EACtB,qBAAqB,EACrB,2BAA2B,EAC3B,gCAAgC,EAChC,gCAAgC,EAChC,qBAAqB,GACtB,MAAM,2BAA2B,CAAC;AACnC,YAAY,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAClF,OAAO,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAC/C,OAAO,EAAE,iBAAiB,EAAE,sBAAsB,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAMnG,OAAO,EACL,sBAAsB,EACtB,wBAAwB,EACxB,oBAAoB,EACpB,qBAAqB,EACrB,kBAAkB,EAClB,oBAAoB,GACrB,MAAM,uBAAuB,CAAC;AAC/B,YAAY,EACV,yBAAyB,EACzB,eAAe,EACf,eAAe,EACf,mBAAmB,EACnB,qBAAqB,GACtB,MAAM,uBAAuB,CAAC"}
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@ export { generateSkybox, skyboxParamsSchema } from './generators/skybox.js';
5
5
  export { generateSoundEffect, soundEffectParamsSchema } from './generators/sound-effect.js';
6
6
  export { generateImageAsset, imageAssetParamsSchema } from './generators/image.js';
7
7
  export { generateCharacter, characterParamsSchema } from './generators/character.js';
8
+ export { generateProp, propParamsSchema } from './generators/prop.js';
8
9
  export { generateAnimationAsset, animationParamsSchema, getConfiguredAnimationModel, ANIMATION_MODEL_TEXT_TO_MOTION_3, ANIMATION_MODEL_TEXT_TO_MOTION_2, ANIMATION_MODEL_OLDER, } from './generators/animation.js';
9
10
  export { fetchContentLength } from './http.js';
10
11
  export { PIXAR_VOXEL_STYLE, MAX_COVER_DESIGN_CHARS, buildCoverArtPrompt } from './cover-prompt.js';
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAC/F,OAAO,EAAE,IAAI,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AACvC,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAEvD,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAE5E,OAAO,EAAE,mBAAmB,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAC;AAE5F,OAAO,EAAE,kBAAkB,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AAEnF,OAAO,EAAE,iBAAiB,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAC;AAErF,OAAO,EACL,sBAAsB,EACtB,qBAAqB,EACrB,2BAA2B,EAC3B,gCAAgC,EAChC,gCAAgC,EAChC,qBAAqB,GACtB,MAAM,2BAA2B,CAAC;AAEnC,OAAO,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAC/C,OAAO,EAAE,iBAAiB,EAAE,sBAAsB,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAEnG,oEAAoE;AACpE,gFAAgF;AAChF,gFAAgF;AAChF,oEAAoE;AACpE,OAAO,EACL,sBAAsB,EACtB,wBAAwB,EACxB,oBAAoB,EACpB,qBAAqB,EACrB,kBAAkB,EAClB,oBAAoB,GACrB,MAAM,uBAAuB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAC/F,OAAO,EAAE,IAAI,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AACvC,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAEvD,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAE5E,OAAO,EAAE,mBAAmB,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAC;AAE5F,OAAO,EAAE,kBAAkB,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AAEnF,OAAO,EAAE,iBAAiB,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAC;AAErF,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAEtE,OAAO,EACL,sBAAsB,EACtB,qBAAqB,EACrB,2BAA2B,EAC3B,gCAAgC,EAChC,gCAAgC,EAChC,qBAAqB,GACtB,MAAM,2BAA2B,CAAC;AAEnC,OAAO,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAC/C,OAAO,EAAE,iBAAiB,EAAE,sBAAsB,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAEnG,oEAAoE;AACpE,gFAAgF;AAChF,gFAAgF;AAChF,oEAAoE;AACpE,OAAO,EACL,sBAAsB,EACtB,wBAAwB,EACxB,oBAAoB,EACpB,qBAAqB,EACrB,kBAAkB,EAClB,oBAAoB,GACrB,MAAM,uBAAuB,CAAC"}
@@ -0,0 +1,260 @@
1
+ /**
2
+ * What "the source of a published game" means, shared by the two sides that must agree on it.
3
+ *
4
+ * The CLI walks a creator's project to produce a fingerprint and a source archive; api-server
5
+ * re-derives the fingerprint from that archive's manifest to check the two describe the same
6
+ * tree. If the exclusion lists, the path normalisation or the fold ever differ between them, a
7
+ * perfectly honest publish reports a mismatch. So they live here, once.
8
+ *
9
+ * Import path matters, twice over:
10
+ *
11
+ * - Published as the `@bitmagic/asset-core/publish-source` subpath, following `./favicon`'s
12
+ * precedent, so consumers do not drag in the root barrel's zod/three/forger client. This is
13
+ * not cosmetic: `api-server/src/cli/cli-routes.ts` documents that eagerly importing the
14
+ * three.js graph into api-server's tests OOM-killed the CI worker. Nothing here may import
15
+ * from `./index.js`.
16
+ * - No zip library, no `fs`. Zip *format* handling stays in each consumer (the CLI writes an
17
+ * archive, api-server streams one apart) and only the pure logic is shared, which keeps this
18
+ * package dependency-clean and keeps the npm blast radius of a change small.
19
+ *
20
+ * The CLI ships to npm with this package pinned, while api-server runs from `main`, so the two
21
+ * routinely run DIFFERENT copies of this file. That is why the fingerprint is versioned — see
22
+ * `SOURCE_INDEX_ALGO`.
23
+ */
24
+ /**
25
+ * The fingerprint algorithm's identity, sent by the CLI and dispatched on by the server.
26
+ *
27
+ * Without it, adding one entry to `EXCLUDED_FILES` — which has happened three times, each for a
28
+ * good reason documented below — would make every publish from an already-installed CLI report a
29
+ * fingerprint mismatch, because that CLI is still hashing the old file set. The server keeps every
30
+ * algorithm it has ever shipped and picks by this value; an unknown or absent one is reported as
31
+ * "cannot check", never as "mismatch". Bump it in lockstep with any change to the two sets below
32
+ * or to `fingerprintFromIndex`.
33
+ */
34
+ export declare const SOURCE_INDEX_ALGO = "v1";
35
+ /**
36
+ * Directories whose contents never change what the bundle contains: our own artifacts
37
+ * (`.bitmagic/`, `dist/`), the dependency tree (`node_modules/` — pinned by the lockfile the
38
+ * scaffold writes), vite's cache, and git's object store. Including any of them would make the
39
+ * fingerprint change on something other than an edit, which would make the staleness gate reject
40
+ * a publish of an unchanged project.
41
+ *
42
+ * `.git` is the one that bit in practice. Hashing it means an EMPTY `git commit` — no source
43
+ * change at all — flips the fingerprint, because the commit writes new objects, a new ref value
44
+ * and a new reflog entry. That turns the ordinary `bitmagic verify` → `git commit` →
45
+ * `bitmagic publish` sequence into exit 3 ("The project has changed since it was last verified"),
46
+ * and `bitmagic upgrade` actively pushes creators into git by refusing a dirty tree. The
47
+ * scaffolded AGENTS.md's "do not edit any tracked file between verifying and publishing" cannot
48
+ * prevent it either: committing is not editing. It also made every publish rebuild, since the
49
+ * build manifest's own recorded fingerprint could never still match by the time publish ran.
50
+ */
51
+ export declare const EXCLUDED_DIRS: ReadonlySet<string>;
52
+ /**
53
+ * Files that can never change what the bundle contains, excluded at basename granularity.
54
+ *
55
+ * `.DS_Store`: on macOS, Finder writes it merely for opening the project folder, and no build tool
56
+ * ever reads it — so it is another way to flip the fingerprint without editing anything.
57
+ *
58
+ * `mechanics-plan.md`: the agent is told to rewrite it after every slice — moving shipped items
59
+ * out of the backlog and re-ranking the rest is the whole point of the file. Left in, the ordinary
60
+ * `bitmagic verify` → update the backlog → `bitmagic publish` sequence exits 3 over prose, which
61
+ * teaches agents to reach for `--force`, the one flag that also waves through a genuinely stale
62
+ * verify.
63
+ *
64
+ * `GAME-DESIGN.md`: same "no build tool reads it" test — nothing imports it,
65
+ * `vite.publish.config.js` cannot put prose in the bundle, and it exists to be REWRITTEN as the
66
+ * game evolves (it is the input `bitmagic cover` reads).
67
+ *
68
+ * The list is deliberately not a general ignore mechanism, because a false "fresh" ships an
69
+ * unverified bundle while a false "stale" only costs one `bitmagic verify`.
70
+ *
71
+ * NOTE the asymmetry with `SECRET_FILE_PATTERNS` below: these files are excluded from the
72
+ * FINGERPRINT but still travel in the source archive, because they are exactly what a human
73
+ * reading the archive wants. Secrets are the other way round.
74
+ */
75
+ export declare const EXCLUDED_FILES: ReadonlySet<string>;
76
+ /**
77
+ * Excluded from the fingerprint, but still carried in the source archive.
78
+ *
79
+ * The two sets answer different questions. The fingerprint asks "would changing this change the
80
+ * bundle?" — for a design document, no, which is why editing one must not invalidate a verify. The
81
+ * archive asks "would a human opening this want it?" — for a design document, emphatically yes: it
82
+ * is the single most useful file for understanding what the game was trying to be.
83
+ *
84
+ * `.DS_Store` is in neither, so it is deliberately absent here: nobody wants it and it is not
85
+ * evidence of anything.
86
+ */
87
+ export declare const ARCHIVED_UNFINGERPRINTED_FILES: ReadonlySet<string>;
88
+ /**
89
+ * Files that must never travel inside the source archive, however the fingerprint treats them.
90
+ *
91
+ * The archive lands in the portal bucket, and that bucket carries a BUCKET-level
92
+ * `allUsers:objectViewer` grant (see `.github/workflows/README.md`) — every object in it is
93
+ * world-readable. A creator's project is their working directory, and working directories collect
94
+ * credentials. Shipping one to a public URL because nobody thought about it is how an incident
95
+ * starts.
96
+ *
97
+ * These files are still hashed and still listed in the manifest (path + hash, no content), so the
98
+ * server can reconcile the fingerprint without ever receiving the bytes. That is the whole reason
99
+ * the manifest is separate from the payload.
100
+ *
101
+ * Deliberately a deny-list of shapes rather than an allow-list of known-good paths: an allow-list
102
+ * would silently drop a creator's own source file the first time someone used an unusual layout,
103
+ * and dropping game code from the archive is a quieter failure than including a stray dotfile.
104
+ * The trade is accepted knowingly — this list will never be complete, which is why the archive's
105
+ * storage key is also unguessable rather than relying on this alone.
106
+ */
107
+ export declare const SECRET_FILE_PATTERNS: readonly RegExp[];
108
+ /** Would including this path in the archive payload risk publishing a credential? */
109
+ export declare function isSecretPath(posixPath: string): boolean;
110
+ /**
111
+ * A relative path as the fingerprint hashes it: always forward-slashed.
112
+ *
113
+ * `sep` is a parameter rather than a direct `path.sep` read so this is testable on any
114
+ * platform — the Windows behaviour that makes normalization necessary cannot otherwise be
115
+ * exercised on the POSIX runners CI uses, which is exactly how a vacuous test gets written.
116
+ */
117
+ export declare function toPosixPath(relativePath: string, sep?: string): string;
118
+ /** One file as both sides see it: the project-relative posix path and the hash of its bytes. */
119
+ export interface SourceIndexEntry {
120
+ path: string;
121
+ sha256: string;
122
+ bytes: number;
123
+ /**
124
+ * For vendored engine files only: the same content hashed with line endings normalised, used
125
+ * exclusively to compare against the official release. See `engineComparisonHash` for why the
126
+ * raw `sha256` above cannot serve — in short, a Windows checkout rewrites LF to CRLF in the
127
+ * committed `engine/` tree and would otherwise report every engine file as modified.
128
+ *
129
+ * Computed here, by the side that actually has the bytes: the archive carries hashes rather than
130
+ * engine content, so the server can never normalise after the fact.
131
+ */
132
+ engineSha256?: string;
133
+ }
134
+ /**
135
+ * Every fingerprinted file in a project, plus the fingerprint they roll up to.
136
+ *
137
+ * Produced once per publish by the CLI (which owns the filesystem walk) and reconstructed by
138
+ * api-server from the archive's manifest. `files` is sorted by path — `fingerprintFromIndex`
139
+ * depends on it and sorts defensively rather than trusting the caller.
140
+ */
141
+ export interface SourceIndex {
142
+ algo: string;
143
+ files: SourceIndexEntry[];
144
+ fingerprint: string;
145
+ /**
146
+ * Files the archive carries but the fingerprint ignores — see
147
+ * `ARCHIVED_UNFINGERPRINTED_FILES`. Kept apart from `files` so they can never reach
148
+ * `fingerprintFromIndex`, which is the whole reason they were excluded in the first place.
149
+ */
150
+ archivedOnly: SourceIndexEntry[];
151
+ }
152
+ /**
153
+ * The fingerprint: a hash of everything that determines the published bundle.
154
+ *
155
+ * This is what lets `publish` decide whether a `verify` is too stale to trust, by CONTENT
156
+ * rather than by clock: a timestamp policy would accept an edit made one second after
157
+ * verifying and reject an untouched project that merely sat overnight.
158
+ *
159
+ * The path is hashed alongside the content so that moving a file — which changes what the
160
+ * bundle imports — changes the fingerprint even though no byte of any file did.
161
+ *
162
+ * Kept deliberately coarse (every tracked file, not a module graph): a false "stale" costs one
163
+ * `bitmagic verify`, while a false "fresh" ships an unverified bundle.
164
+ *
165
+ * The exact byte sequence folded here is load-bearing across two independently-released
166
+ * packages. Do not "clean up" the trailing newline or the separator without bumping
167
+ * `SOURCE_INDEX_ALGO`.
168
+ */
169
+ export declare function fingerprintFromIndex(files: readonly SourceIndexEntry[]): string;
170
+ /** Where the archive's manifest lives inside the zip. */
171
+ export declare const SOURCE_MANIFEST_NAME = "bitmagic-source.json";
172
+ /**
173
+ * The archive's manifest: enough for the server to re-derive the fingerprint and to check the
174
+ * vendored engine, without the archive having to carry either the engine or any secret.
175
+ *
176
+ * `omitted` lists the paths that are in `files` but deliberately absent from the payload, so a
177
+ * reader (human or server) can tell "not shipped, on purpose" from "the archive is truncated".
178
+ *
179
+ * Deliberately NO `publishVersion`. The archive has to be built and hashed BEFORE `publish/begin`
180
+ * — that hash is what `begin` bakes into the meta block — and the version is not allocated until
181
+ * `begin` returns. A field that cannot be known when the file is written cannot be in the file.
182
+ * The version is recorded where it is actually available: in the archive's storage key and in the
183
+ * meta block's own `bm:publish-version`.
184
+ */
185
+ export interface SourceManifest {
186
+ algo: string;
187
+ gameId: string;
188
+ engineVersion: string;
189
+ genre: string;
190
+ exportedAt: string;
191
+ fingerprint: string;
192
+ files: Record<string, string>;
193
+ omitted: string[];
194
+ /**
195
+ * Line-ending-normalised hashes for the vendored engine paths only, keyed the same way as
196
+ * `files`. Separate from `files` because the two answer different questions: `files` must stay
197
+ * byte-exact for the fingerprint, while this exists to survive a platform's checkout rules.
198
+ */
199
+ engineHashes: Record<string, string>;
200
+ }
201
+ /**
202
+ * Project-relative prefix of the vendored engine. `bitmagic init`/`upgrade` unpack the release
203
+ * tarball's `game/src/<root>` into `engine/<root>`, so every vendored file lives under this.
204
+ */
205
+ export declare const VENDORED_ENGINE_PREFIX = "engine/";
206
+ /**
207
+ * Map a path as it appears in the engine release tarball to where it lands in a scaffolded
208
+ * project, or null when the tarball entry is not vendored into projects at all.
209
+ *
210
+ * The tarball stores three shapes at different depths (see `cli/src/scaffold/engine-download.ts`):
211
+ * `game/src/<root>/…` → `engine/<root>/…`, `game/agent-docs/…` → `engine/agent-docs/…`, and
212
+ * `game/sw-cache-buster.js` → `sw-cache-buster.js` at the project root. `templates/` ships in the
213
+ * tarball for the scaffolder to read and is never vendored, so it maps to null.
214
+ *
215
+ * Lives here rather than in api-server because it is a statement about what the CLI did to the
216
+ * tarball, and the CLI is the other consumer that has to agree.
217
+ */
218
+ export declare function tarballPathToProjectPath(tarballPath: string): string | null;
219
+ /**
220
+ * Bytes every genuine published bundle carries, used to refuse a publish that is not an engine
221
+ * build at all.
222
+ *
223
+ * These survive the toolchain verbatim, which is the entire reason they were chosen over anything
224
+ * cleverer. The scaffold's `index.html` emits `<div id="game-container">` and a
225
+ * `<script type="importmap">` whose body Vite only RELOCATES (its import-map hook lifts the tag
226
+ * out and reinserts the matched text unchanged before the first module script) — so both land, byte
227
+ * for byte, a few KB into the single-file bundle, well inside the head range `complete` already
228
+ * reads for the meta block. Verified against a real `vite build` with `vite-plugin-singlefile`,
229
+ * not inferred.
230
+ *
231
+ * Why a SUBSET of the alias entries rather than all of them. A creator's installed CLI is an older
232
+ * npm release than the running server, so the server must only require markers that both agree on.
233
+ * The CDN entries are disqualified outright — they embed the pinned three.js version, which moves
234
+ * whenever the CLI bumps it. Of the local aliases these three are the ones that have to exist for
235
+ * a project to resolve its entry module at all, so requiring more would buy nothing and would turn
236
+ * any future alias change into a spurious refusal for every not-yet-upgraded project.
237
+ *
238
+ * What this is worth: it stops bitmagic.ai being used to host arbitrary HTML by someone who has an
239
+ * account and a game id. It is trivially copy-pasteable by anyone who looks, and is not claimed to
240
+ * be more than that.
241
+ */
242
+ export declare const ENGINE_BUILD_MARKERS: readonly string[];
243
+ /** Does this bundle's head carry every marker a genuine engine build has? */
244
+ export declare function hasEngineBuildMarkers(head: string): boolean;
245
+ /**
246
+ * Hash used ONLY when comparing a vendored engine file against the official release.
247
+ *
248
+ * Line endings are normalised first, which the fingerprint deliberately never does. The reason is
249
+ * Windows: `renderGitignore()` does not ignore `engine/`, so the vendored tree is committed, and
250
+ * AGENTS.md pushes creators into git (`bitmagic upgrade` refuses a dirty tree). A Windows checkout
251
+ * with the default `core.autocrlf=true` rewrites LF to CRLF in every `.ts` file it restores — so
252
+ * without this, every Windows creator would see 100% of their engine files reported as modified,
253
+ * forever, on every publish. That is a false alarm about the one check most likely to be believed.
254
+ *
255
+ * Confined to this comparison on purpose. Normalising inside the fingerprint would instead make
256
+ * the fingerprint disagree with itself across platforms in the staleness gate, where a false
257
+ * "fresh" is the dangerous direction.
258
+ */
259
+ export declare function engineComparisonHash(content: Uint8Array): string;
260
+ //# sourceMappingURL=publish-source.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"publish-source.d.ts","sourceRoot":"","sources":["../src/publish-source.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAQH;;;;;;;;;GASG;AACH,eAAO,MAAM,iBAAiB,OAAO,CAAC;AAEtC;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,aAAa,EAAE,WAAW,CAAC,MAAM,CAM5C,CAAC;AAEH;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,eAAO,MAAM,cAAc,EAAE,WAAW,CAAC,MAAM,CAI7C,CAAC;AAEH;;;;;;;;;;GAUG;AACH,eAAO,MAAM,8BAA8B,EAAE,WAAW,CAAC,MAAM,CAG7D,CAAC;AAEH;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,oBAAoB,EAAE,SAAS,MAAM,EASjD,CAAC;AAEF,qFAAqF;AACrF,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAEvD;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,YAAY,EAAE,MAAM,EAAE,GAAG,GAAE,MAAY,GAAG,MAAM,CAE3E;AAED,gGAAgG;AAChG,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd;;;;;;;;OAQG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,gBAAgB,EAAE,CAAC;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,YAAY,EAAE,gBAAgB,EAAE,CAAC;CAClC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,SAAS,gBAAgB,EAAE,GAAG,MAAM,CAO/E;AAED,yDAAyD;AACzD,eAAO,MAAM,oBAAoB,yBAAyB,CAAC;AAE3D;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,EAAE,MAAM,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB;;;;OAIG;IACH,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACtC;AAED;;;GAGG;AACH,eAAO,MAAM,sBAAsB,YAAY,CAAC;AAEhD;;;;;;;;;;;GAWG;AACH,wBAAgB,wBAAwB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAW3E;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,eAAO,MAAM,oBAAoB,EAAE,SAAS,MAAM,EAIjD,CAAC;AAEF,6EAA6E;AAC7E,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAE3D;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,UAAU,GAAG,MAAM,CAMhE"}
@@ -0,0 +1,258 @@
1
+ /**
2
+ * What "the source of a published game" means, shared by the two sides that must agree on it.
3
+ *
4
+ * The CLI walks a creator's project to produce a fingerprint and a source archive; api-server
5
+ * re-derives the fingerprint from that archive's manifest to check the two describe the same
6
+ * tree. If the exclusion lists, the path normalisation or the fold ever differ between them, a
7
+ * perfectly honest publish reports a mismatch. So they live here, once.
8
+ *
9
+ * Import path matters, twice over:
10
+ *
11
+ * - Published as the `@bitmagic/asset-core/publish-source` subpath, following `./favicon`'s
12
+ * precedent, so consumers do not drag in the root barrel's zod/three/forger client. This is
13
+ * not cosmetic: `api-server/src/cli/cli-routes.ts` documents that eagerly importing the
14
+ * three.js graph into api-server's tests OOM-killed the CI worker. Nothing here may import
15
+ * from `./index.js`.
16
+ * - No zip library, no `fs`. Zip *format* handling stays in each consumer (the CLI writes an
17
+ * archive, api-server streams one apart) and only the pure logic is shared, which keeps this
18
+ * package dependency-clean and keeps the npm blast radius of a change small.
19
+ *
20
+ * The CLI ships to npm with this package pinned, while api-server runs from `main`, so the two
21
+ * routinely run DIFFERENT copies of this file. That is why the fingerprint is versioned — see
22
+ * `SOURCE_INDEX_ALGO`.
23
+ */
24
+ import { createHash } from 'node:crypto';
25
+ // Imported rather than taken from the ambient global, matching `node:crypto` above: this file's
26
+ // eslint config enumerates its globals deliberately, and an explicit import says "node-only" as
27
+ // clearly as the crypto one does.
28
+ import { TextDecoder } from 'node:util';
29
+ /**
30
+ * The fingerprint algorithm's identity, sent by the CLI and dispatched on by the server.
31
+ *
32
+ * Without it, adding one entry to `EXCLUDED_FILES` — which has happened three times, each for a
33
+ * good reason documented below — would make every publish from an already-installed CLI report a
34
+ * fingerprint mismatch, because that CLI is still hashing the old file set. The server keeps every
35
+ * algorithm it has ever shipped and picks by this value; an unknown or absent one is reported as
36
+ * "cannot check", never as "mismatch". Bump it in lockstep with any change to the two sets below
37
+ * or to `fingerprintFromIndex`.
38
+ */
39
+ export const SOURCE_INDEX_ALGO = 'v1';
40
+ /**
41
+ * Directories whose contents never change what the bundle contains: our own artifacts
42
+ * (`.bitmagic/`, `dist/`), the dependency tree (`node_modules/` — pinned by the lockfile the
43
+ * scaffold writes), vite's cache, and git's object store. Including any of them would make the
44
+ * fingerprint change on something other than an edit, which would make the staleness gate reject
45
+ * a publish of an unchanged project.
46
+ *
47
+ * `.git` is the one that bit in practice. Hashing it means an EMPTY `git commit` — no source
48
+ * change at all — flips the fingerprint, because the commit writes new objects, a new ref value
49
+ * and a new reflog entry. That turns the ordinary `bitmagic verify` → `git commit` →
50
+ * `bitmagic publish` sequence into exit 3 ("The project has changed since it was last verified"),
51
+ * and `bitmagic upgrade` actively pushes creators into git by refusing a dirty tree. The
52
+ * scaffolded AGENTS.md's "do not edit any tracked file between verifying and publishing" cannot
53
+ * prevent it either: committing is not editing. It also made every publish rebuild, since the
54
+ * build manifest's own recorded fingerprint could never still match by the time publish ran.
55
+ */
56
+ export const EXCLUDED_DIRS = new Set([
57
+ '.bitmagic',
58
+ 'node_modules',
59
+ 'dist',
60
+ '.vite-cache',
61
+ '.git',
62
+ ]);
63
+ /**
64
+ * Files that can never change what the bundle contains, excluded at basename granularity.
65
+ *
66
+ * `.DS_Store`: on macOS, Finder writes it merely for opening the project folder, and no build tool
67
+ * ever reads it — so it is another way to flip the fingerprint without editing anything.
68
+ *
69
+ * `mechanics-plan.md`: the agent is told to rewrite it after every slice — moving shipped items
70
+ * out of the backlog and re-ranking the rest is the whole point of the file. Left in, the ordinary
71
+ * `bitmagic verify` → update the backlog → `bitmagic publish` sequence exits 3 over prose, which
72
+ * teaches agents to reach for `--force`, the one flag that also waves through a genuinely stale
73
+ * verify.
74
+ *
75
+ * `GAME-DESIGN.md`: same "no build tool reads it" test — nothing imports it,
76
+ * `vite.publish.config.js` cannot put prose in the bundle, and it exists to be REWRITTEN as the
77
+ * game evolves (it is the input `bitmagic cover` reads).
78
+ *
79
+ * The list is deliberately not a general ignore mechanism, because a false "fresh" ships an
80
+ * unverified bundle while a false "stale" only costs one `bitmagic verify`.
81
+ *
82
+ * NOTE the asymmetry with `SECRET_FILE_PATTERNS` below: these files are excluded from the
83
+ * FINGERPRINT but still travel in the source archive, because they are exactly what a human
84
+ * reading the archive wants. Secrets are the other way round.
85
+ */
86
+ export const EXCLUDED_FILES = new Set([
87
+ '.DS_Store',
88
+ 'GAME-DESIGN.md',
89
+ 'mechanics-plan.md',
90
+ ]);
91
+ /**
92
+ * Excluded from the fingerprint, but still carried in the source archive.
93
+ *
94
+ * The two sets answer different questions. The fingerprint asks "would changing this change the
95
+ * bundle?" — for a design document, no, which is why editing one must not invalidate a verify. The
96
+ * archive asks "would a human opening this want it?" — for a design document, emphatically yes: it
97
+ * is the single most useful file for understanding what the game was trying to be.
98
+ *
99
+ * `.DS_Store` is in neither, so it is deliberately absent here: nobody wants it and it is not
100
+ * evidence of anything.
101
+ */
102
+ export const ARCHIVED_UNFINGERPRINTED_FILES = new Set([
103
+ 'GAME-DESIGN.md',
104
+ 'mechanics-plan.md',
105
+ ]);
106
+ /**
107
+ * Files that must never travel inside the source archive, however the fingerprint treats them.
108
+ *
109
+ * The archive lands in the portal bucket, and that bucket carries a BUCKET-level
110
+ * `allUsers:objectViewer` grant (see `.github/workflows/README.md`) — every object in it is
111
+ * world-readable. A creator's project is their working directory, and working directories collect
112
+ * credentials. Shipping one to a public URL because nobody thought about it is how an incident
113
+ * starts.
114
+ *
115
+ * These files are still hashed and still listed in the manifest (path + hash, no content), so the
116
+ * server can reconcile the fingerprint without ever receiving the bytes. That is the whole reason
117
+ * the manifest is separate from the payload.
118
+ *
119
+ * Deliberately a deny-list of shapes rather than an allow-list of known-good paths: an allow-list
120
+ * would silently drop a creator's own source file the first time someone used an unusual layout,
121
+ * and dropping game code from the archive is a quieter failure than including a stray dotfile.
122
+ * The trade is accepted knowingly — this list will never be complete, which is why the archive's
123
+ * storage key is also unguessable rather than relying on this alone.
124
+ */
125
+ export const SECRET_FILE_PATTERNS = [
126
+ /(^|\/)\.env($|\.)/,
127
+ /\.pem$/,
128
+ /\.key$/,
129
+ /(^|\/)id_rsa/,
130
+ /(^|\/)\.npmrc$/,
131
+ /(^|\/)\.git-credentials$/,
132
+ /service-account[^/]*\.json$/,
133
+ /(^|\/)\.claude\/settings\.local\.json$/,
134
+ ];
135
+ /** Would including this path in the archive payload risk publishing a credential? */
136
+ export function isSecretPath(posixPath) {
137
+ return SECRET_FILE_PATTERNS.some((pattern) => pattern.test(posixPath));
138
+ }
139
+ /**
140
+ * A relative path as the fingerprint hashes it: always forward-slashed.
141
+ *
142
+ * `sep` is a parameter rather than a direct `path.sep` read so this is testable on any
143
+ * platform — the Windows behaviour that makes normalization necessary cannot otherwise be
144
+ * exercised on the POSIX runners CI uses, which is exactly how a vacuous test gets written.
145
+ */
146
+ export function toPosixPath(relativePath, sep = '/') {
147
+ return relativePath.split(sep).join('/');
148
+ }
149
+ /**
150
+ * The fingerprint: a hash of everything that determines the published bundle.
151
+ *
152
+ * This is what lets `publish` decide whether a `verify` is too stale to trust, by CONTENT
153
+ * rather than by clock: a timestamp policy would accept an edit made one second after
154
+ * verifying and reject an untouched project that merely sat overnight.
155
+ *
156
+ * The path is hashed alongside the content so that moving a file — which changes what the
157
+ * bundle imports — changes the fingerprint even though no byte of any file did.
158
+ *
159
+ * Kept deliberately coarse (every tracked file, not a module graph): a false "stale" costs one
160
+ * `bitmagic verify`, while a false "fresh" ships an unverified bundle.
161
+ *
162
+ * The exact byte sequence folded here is load-bearing across two independently-released
163
+ * packages. Do not "clean up" the trailing newline or the separator without bumping
164
+ * `SOURCE_INDEX_ALGO`.
165
+ */
166
+ export function fingerprintFromIndex(files) {
167
+ const sorted = [...files].sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
168
+ const hash = createHash('sha256');
169
+ for (const entry of sorted) {
170
+ hash.update(`${entry.path}:${entry.sha256}\n`);
171
+ }
172
+ return hash.digest('hex');
173
+ }
174
+ /** Where the archive's manifest lives inside the zip. */
175
+ export const SOURCE_MANIFEST_NAME = 'bitmagic-source.json';
176
+ /**
177
+ * Project-relative prefix of the vendored engine. `bitmagic init`/`upgrade` unpack the release
178
+ * tarball's `game/src/<root>` into `engine/<root>`, so every vendored file lives under this.
179
+ */
180
+ export const VENDORED_ENGINE_PREFIX = 'engine/';
181
+ /**
182
+ * Map a path as it appears in the engine release tarball to where it lands in a scaffolded
183
+ * project, or null when the tarball entry is not vendored into projects at all.
184
+ *
185
+ * The tarball stores three shapes at different depths (see `cli/src/scaffold/engine-download.ts`):
186
+ * `game/src/<root>/…` → `engine/<root>/…`, `game/agent-docs/…` → `engine/agent-docs/…`, and
187
+ * `game/sw-cache-buster.js` → `sw-cache-buster.js` at the project root. `templates/` ships in the
188
+ * tarball for the scaffolder to read and is never vendored, so it maps to null.
189
+ *
190
+ * Lives here rather than in api-server because it is a statement about what the CLI did to the
191
+ * tarball, and the CLI is the other consumer that has to agree.
192
+ */
193
+ export function tarballPathToProjectPath(tarballPath) {
194
+ if (tarballPath.startsWith('game/src/')) {
195
+ return `${VENDORED_ENGINE_PREFIX}${tarballPath.slice('game/src/'.length)}`;
196
+ }
197
+ if (tarballPath.startsWith('game/agent-docs/')) {
198
+ return `${VENDORED_ENGINE_PREFIX}agent-docs/${tarballPath.slice('game/agent-docs/'.length)}`;
199
+ }
200
+ if (tarballPath === 'game/sw-cache-buster.js') {
201
+ return 'sw-cache-buster.js';
202
+ }
203
+ return null;
204
+ }
205
+ /**
206
+ * Bytes every genuine published bundle carries, used to refuse a publish that is not an engine
207
+ * build at all.
208
+ *
209
+ * These survive the toolchain verbatim, which is the entire reason they were chosen over anything
210
+ * cleverer. The scaffold's `index.html` emits `<div id="game-container">` and a
211
+ * `<script type="importmap">` whose body Vite only RELOCATES (its import-map hook lifts the tag
212
+ * out and reinserts the matched text unchanged before the first module script) — so both land, byte
213
+ * for byte, a few KB into the single-file bundle, well inside the head range `complete` already
214
+ * reads for the meta block. Verified against a real `vite build` with `vite-plugin-singlefile`,
215
+ * not inferred.
216
+ *
217
+ * Why a SUBSET of the alias entries rather than all of them. A creator's installed CLI is an older
218
+ * npm release than the running server, so the server must only require markers that both agree on.
219
+ * The CDN entries are disqualified outright — they embed the pinned three.js version, which moves
220
+ * whenever the CLI bumps it. Of the local aliases these three are the ones that have to exist for
221
+ * a project to resolve its entry module at all, so requiring more would buy nothing and would turn
222
+ * any future alias change into a spurious refusal for every not-yet-upgraded project.
223
+ *
224
+ * What this is worth: it stops bitmagic.ai being used to host arbitrary HTML by someone who has an
225
+ * account and a game id. It is trivially copy-pasteable by anyone who looks, and is not claimed to
226
+ * be more than that.
227
+ */
228
+ export const ENGINE_BUILD_MARKERS = [
229
+ 'id="game-container"',
230
+ '"engine/": "/dist/engine/engine/"',
231
+ '"types/": "/dist/engine/types/"',
232
+ ];
233
+ /** Does this bundle's head carry every marker a genuine engine build has? */
234
+ export function hasEngineBuildMarkers(head) {
235
+ return ENGINE_BUILD_MARKERS.every((marker) => head.includes(marker));
236
+ }
237
+ /**
238
+ * Hash used ONLY when comparing a vendored engine file against the official release.
239
+ *
240
+ * Line endings are normalised first, which the fingerprint deliberately never does. The reason is
241
+ * Windows: `renderGitignore()` does not ignore `engine/`, so the vendored tree is committed, and
242
+ * AGENTS.md pushes creators into git (`bitmagic upgrade` refuses a dirty tree). A Windows checkout
243
+ * with the default `core.autocrlf=true` rewrites LF to CRLF in every `.ts` file it restores — so
244
+ * without this, every Windows creator would see 100% of their engine files reported as modified,
245
+ * forever, on every publish. That is a false alarm about the one check most likely to be believed.
246
+ *
247
+ * Confined to this comparison on purpose. Normalising inside the fingerprint would instead make
248
+ * the fingerprint disagree with itself across platforms in the staleness gate, where a false
249
+ * "fresh" is the dangerous direction.
250
+ */
251
+ export function engineComparisonHash(content) {
252
+ // TextDecoder rather than Buffer: this module is imported by an npm-published CLI as well as by
253
+ // the server, and keeping Node's Buffer out of it means the file has no ambient-global
254
+ // dependency beyond `node:crypto`, which it imports explicitly.
255
+ const text = new TextDecoder('utf-8').decode(content);
256
+ return createHash('sha256').update(text.replace(/\r\n/g, '\n'), 'utf8').digest('hex');
257
+ }
258
+ //# sourceMappingURL=publish-source.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"publish-source.js","sourceRoot":"","sources":["../src/publish-source.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,gGAAgG;AAChG,gGAAgG;AAChG,kCAAkC;AAClC,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAExC;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,IAAI,CAAC;AAEtC;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,MAAM,aAAa,GAAwB,IAAI,GAAG,CAAC;IACxD,WAAW;IACX,cAAc;IACd,MAAM;IACN,aAAa;IACb,MAAM;CACP,CAAC,CAAC;AAEH;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,CAAC,MAAM,cAAc,GAAwB,IAAI,GAAG,CAAC;IACzD,WAAW;IACX,gBAAgB;IAChB,mBAAmB;CACpB,CAAC,CAAC;AAEH;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,8BAA8B,GAAwB,IAAI,GAAG,CAAC;IACzE,gBAAgB;IAChB,mBAAmB;CACpB,CAAC,CAAC;AAEH;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAsB;IACrD,mBAAmB;IACnB,QAAQ;IACR,QAAQ;IACR,cAAc;IACd,gBAAgB;IAChB,0BAA0B;IAC1B,6BAA6B;IAC7B,wCAAwC;CACzC,CAAC;AAEF,qFAAqF;AACrF,MAAM,UAAU,YAAY,CAAC,SAAiB;IAC5C,OAAO,oBAAoB,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC;AACzE,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,WAAW,CAAC,YAAoB,EAAE,MAAc,GAAG;IACjE,OAAO,YAAY,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAC3C,CAAC;AAsCD;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,oBAAoB,CAAC,KAAkC;IACrE,MAAM,MAAM,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC3F,MAAM,IAAI,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC;IAClC,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,IAAI,CAAC,MAAM,CAAC,GAAG,KAAK,CAAC,IAAI,IAAI,KAAK,CAAC,MAAM,IAAI,CAAC,CAAC;IACjD,CAAC;IACD,OAAO,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAC5B,CAAC;AAED,yDAAyD;AACzD,MAAM,CAAC,MAAM,oBAAoB,GAAG,sBAAsB,CAAC;AAgC3D;;;GAGG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,SAAS,CAAC;AAEhD;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,wBAAwB,CAAC,WAAmB;IAC1D,IAAI,WAAW,CAAC,UAAU,CAAC,WAAW,CAAC,EAAE,CAAC;QACxC,OAAO,GAAG,sBAAsB,GAAG,WAAW,CAAC,KAAK,CAAC,WAAW,CAAC,MAAM,CAAC,EAAE,CAAC;IAC7E,CAAC;IACD,IAAI,WAAW,CAAC,UAAU,CAAC,kBAAkB,CAAC,EAAE,CAAC;QAC/C,OAAO,GAAG,sBAAsB,cAAc,WAAW,CAAC,KAAK,CAAC,kBAAkB,CAAC,MAAM,CAAC,EAAE,CAAC;IAC/F,CAAC;IACD,IAAI,WAAW,KAAK,yBAAyB,EAAE,CAAC;QAC9C,OAAO,oBAAoB,CAAC;IAC9B,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAsB;IACrD,qBAAqB;IACrB,mCAAmC;IACnC,iCAAiC;CAClC,CAAC;AAEF,6EAA6E;AAC7E,MAAM,UAAU,qBAAqB,CAAC,IAAY;IAChD,OAAO,oBAAoB,CAAC,KAAK,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC;AACvE,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,oBAAoB,CAAC,OAAmB;IACtD,gGAAgG;IAChG,uFAAuF;IACvF,gEAAgE;IAChE,MAAM,IAAI,GAAG,IAAI,WAAW,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IACtD,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AACxF,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bitmagic/asset-core",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -13,6 +13,14 @@
13
13
  "types": "./dist/terminal-failure.d.ts",
14
14
  "default": "./dist/terminal-failure.js"
15
15
  },
16
+ "./favicon": {
17
+ "types": "./dist/favicon.d.ts",
18
+ "default": "./dist/favicon.js"
19
+ },
20
+ "./publish-source": {
21
+ "types": "./dist/publish-source.d.ts",
22
+ "default": "./dist/publish-source.js"
23
+ },
16
24
  "./animation/*": {
17
25
  "default": "./src/animation/*"
18
26
  },