playcademy 0.24.1-beta.5 → 0.25.0-beta.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.
@@ -0,0 +1,214 @@
1
+ import { TimebackCourseConfig, CourseConfig, OrganizationConfig, ComponentConfig, ResourceConfig, ComponentResourceConfig } from '@playcademy/types/timeback';
2
+ import { Miniflare } from 'miniflare';
3
+
4
+ /**
5
+ * @fileoverview Server SDK Type Definitions
6
+ *
7
+ * TypeScript type definitions for the server-side Playcademy SDK.
8
+ * Includes configuration types, client state, and re-exported TimeBack types.
9
+ */
10
+
11
+ /**
12
+ * Base configuration for TimeBack integration (shared across all courses).
13
+ * References upstream TimeBack types from @playcademy/timeback.
14
+ *
15
+ * All fields are optional and support template variables: {grade}, {subject}, {gameSlug}
16
+ */
17
+ interface TimebackBaseConfig {
18
+ /** Organization configuration (shared across all courses) */
19
+ organization?: Partial<OrganizationConfig>;
20
+ /** Course defaults (can be overridden per-course) */
21
+ course?: Partial<CourseConfig>;
22
+ /** Component defaults */
23
+ component?: Partial<ComponentConfig>;
24
+ /** Resource defaults */
25
+ resource?: Partial<ResourceConfig>;
26
+ /** ComponentResource defaults */
27
+ componentResource?: Partial<ComponentResourceConfig>;
28
+ }
29
+ /**
30
+ * Extended course configuration that merges TimebackCourseConfig with per-course overrides.
31
+ * Used in playcademy.config.* to allow per-course customization.
32
+ */
33
+ interface TimebackCourseConfigWithOverrides extends TimebackCourseConfig {
34
+ title?: string;
35
+ courseCode?: string;
36
+ level?: string;
37
+ metadata?: CourseConfig['metadata'];
38
+ totalXp?: number | null;
39
+ masterableUnits?: number | null;
40
+ }
41
+ /**
42
+ * TimeBack integration configuration for Playcademy config file.
43
+ *
44
+ * Supports two levels of customization:
45
+ * 1. `base`: Shared defaults for all courses (organization, course, component, resource, componentResource)
46
+ * 2. Per-course overrides in the `courses` array (title, courseCode, level, gradingScheme, metadata)
47
+ *
48
+ * Template variables ({grade}, {subject}, {gameSlug}) can be used in string fields.
49
+ */
50
+ interface TimebackIntegrationConfig {
51
+ /** Multi-grade course configuration (array of grade/subject/totalXp with optional per-course overrides) */
52
+ courses: TimebackCourseConfigWithOverrides[];
53
+ /** Optional base configuration (shared across all courses, can be overridden per-course) */
54
+ base?: TimebackBaseConfig;
55
+ }
56
+ /**
57
+ * Custom API routes integration
58
+ */
59
+ interface CustomRoutesIntegration {
60
+ /** Directory for custom API routes (defaults to 'server/api') */
61
+ directory?: string;
62
+ }
63
+ /**
64
+ * Bucket storage integration
65
+ */
66
+ interface BucketIntegration {
67
+ /**
68
+ * Directory whose contents are synced to the bucket by `playcademy bucket
69
+ * sync` (defaults to 'bucket'). Synced as a single content-addressed tree:
70
+ * the whole directory is the manifest's source of truth.
71
+ */
72
+ directory?: string;
73
+ }
74
+ /**
75
+ * Database integration
76
+ */
77
+ interface DatabaseIntegration {
78
+ /** Database directory (defaults to 'db') */
79
+ directory?: string;
80
+ /** Schema strategy: 'push' uses drizzle-kit push-style diffing, 'migrate' uses migration files.
81
+ * When omitted, auto-detects based on presence of a migrations directory with _journal.json. */
82
+ strategy?: 'push' | 'migrate';
83
+ }
84
+ interface QueueConfig {
85
+ maxBatchSize?: number;
86
+ maxRetries?: number;
87
+ maxBatchTimeout?: number;
88
+ maxConcurrency?: number;
89
+ retryDelay?: number;
90
+ deadLetterQueue?: string;
91
+ }
92
+ /**
93
+ * Integrations configuration
94
+ * All backend features (database, custom routes, external services) are configured here
95
+ */
96
+ interface IntegrationsConfig {
97
+ /** TimeBack integration (optional) */
98
+ timeback?: TimebackIntegrationConfig | null;
99
+ /** Custom API routes (optional) */
100
+ customRoutes?: CustomRoutesIntegration | boolean;
101
+ /** Database (optional) */
102
+ database?: DatabaseIntegration | boolean;
103
+ /** Key-Value storage (optional) */
104
+ kv?: boolean;
105
+ /** Bucket storage (optional) */
106
+ bucket?: BucketIntegration | boolean;
107
+ /** Authentication (optional) */
108
+ auth?: boolean;
109
+ /** Queues (optional) */
110
+ queues?: Record<string, QueueConfig | boolean>;
111
+ }
112
+ /**
113
+ * Unified Playcademy configuration
114
+ * Used for playcademy.config.{js,json}
115
+ */
116
+ interface PlaycademyConfig {
117
+ /** Game name */
118
+ name: string;
119
+ /** Game description */
120
+ description?: string;
121
+ /** Game emoji icon */
122
+ emoji?: string;
123
+ /** Build command to run before deployment */
124
+ buildCommand?: string[];
125
+ /** Path to build output */
126
+ buildPath?: string;
127
+ /** Game type */
128
+ gameType?: 'hosted' | 'external';
129
+ /** External URL (for external games) */
130
+ externalUrl?: string;
131
+ /** Game platform */
132
+ platform?: 'web' | 'godot';
133
+ /** Integrations (database, custom routes, external services) */
134
+ integrations?: IntegrationsConfig;
135
+ }
136
+
137
+ /**
138
+ * Local Bucket Access
139
+ *
140
+ * One place that knows how to open the local R2 bucket (a Miniflare instance
141
+ * persisted under the workspace). Every `bucket` command that touches local
142
+ * storage goes through {@link withLocalBucket} so the Miniflare config lives in
143
+ * a single spot and the instance is always disposed.
144
+ */
145
+
146
+ /** The local R2 binding handed back by Miniflare's `getR2Bucket`. */
147
+ type LocalBucket = Awaited<ReturnType<Miniflare['getR2Bucket']>>;
148
+ /** A persistent local bucket handle the caller is responsible for disposing. */
149
+ interface LocalBucketSession {
150
+ bucket: LocalBucket;
151
+ dispose: () => Promise<void>;
152
+ }
153
+
154
+ /**
155
+ * Local Asset Dev Support
156
+ *
157
+ * Helpers the Vite plugin uses to give `asset()` a working local-dev story.
158
+ * In production the edge worker injects the asset manifest into the page and
159
+ * serves `/api/assets/*` from R2; in `vite dev` there is no edge worker, so the
160
+ * plugin reproduces both against the local Miniflare bucket that `bucket sync`
161
+ * writes to. Keeping this here means the Miniflare lifecycle and the `_assets/`
162
+ * layout live in one place — the plugin never touches Miniflare directly.
163
+ */
164
+
165
+ /** The serving base the manifest is injected with (matches the edge worker). */
166
+ declare const ASSET_DEV_ROUTE_PREFIX = "/api/assets/";
167
+ /**
168
+ * Whether the game defines a custom route that owns `/api/assets/*`.
169
+ *
170
+ * In production a game's `server/api/assets` route supersedes the built-in
171
+ * content-addressed asset route (Hono first-match-wins). Dev must honor the
172
+ * same precedence: when this returns true, the Vite plugin should NOT register
173
+ * its `/api/assets/*` middleware, letting requests proxy to the backend where
174
+ * the custom route runs — matching deployed behavior.
175
+ *
176
+ * A route owning that path lives at `<customRoutesDir>/assets.ts` or
177
+ * `<customRoutesDir>/assets/...` (e.g. `assets/index.ts`, `assets/[...path].ts`),
178
+ * so the presence of an `assets` file or directory there is sufficient.
179
+ */
180
+ declare function hasCustomAssetsRoute(projectRoot: string, config?: PlaycademyConfig | null): boolean;
181
+ /**
182
+ * Open a persistent local bucket for asset dev serving.
183
+ *
184
+ * The Vite plugin opens one of these for the dev server's lifetime and reads
185
+ * the manifest and assets from it, rather than spinning up a Miniflare instance
186
+ * per request. The handle stays fresh across `bucket sync` runs (see
187
+ * {@link openLocalBucket}); the caller disposes it when the dev server stops.
188
+ */
189
+ declare function openLocalAssetBucket(): Promise<LocalBucketSession>;
190
+ /**
191
+ * Build the `<script>` that exposes the asset manifest on
192
+ * `self.__PLAYCADEMY_ASSET_MANIFEST`, read from the given local bucket.
193
+ *
194
+ * Byte-for-byte the same shape the edge worker injects in production (the
195
+ * stored manifest holds bare content keys; `base` is spliced in here), so
196
+ * `asset()` resolves identically in dev and on the platform. Returns `null`
197
+ * when the local bucket has no manifest yet (no `bucket sync` has run).
198
+ */
199
+ declare function readLocalManifestScript(bucket: LocalBucket): Promise<string | null>;
200
+ /** Bytes + content type for a served asset. */
201
+ interface LocalAsset {
202
+ body: Uint8Array;
203
+ contentType: string;
204
+ }
205
+ /**
206
+ * Read an asset's bytes from the given local bucket by its serving path — the
207
+ * part after `/api/assets/` (e.g. `sprites/hero.a1b2c3d4.svg`). Mirrors the edge
208
+ * asset route: the request path maps to `${ASSET_PREFIX}${servingKey}` in R2.
209
+ * Returns `null` when the object isn't present.
210
+ */
211
+ declare function readLocalAsset(bucket: LocalBucket, servingKey: string): Promise<LocalAsset | null>;
212
+
213
+ export { ASSET_DEV_ROUTE_PREFIX, hasCustomAssetsRoute, openLocalAssetBucket, readLocalAsset, readLocalManifestScript };
214
+ export type { LocalAsset, LocalBucket, LocalBucketSession };
package/dist/bucket.js ADDED
@@ -0,0 +1,341 @@
1
+ var __getOwnPropNames = Object.getOwnPropertyNames;
2
+ var __esm = (fn, res) => function __init() {
3
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
4
+ };
5
+
6
+ // ../utils/src/package-json.ts
7
+ var init_package_json = __esm({
8
+ "../utils/src/package-json.ts"() {
9
+ "use strict";
10
+ }
11
+ });
12
+
13
+ // ../utils/src/file-loader.ts
14
+ var init_file_loader = __esm({
15
+ "../utils/src/file-loader.ts"() {
16
+ "use strict";
17
+ init_package_json();
18
+ }
19
+ });
20
+
21
+ // src/lib/bucket/dev.ts
22
+ import { existsSync } from "fs";
23
+ import { join as join8 } from "path";
24
+
25
+ // ../constants/src/timeback.ts
26
+ var TIMEBACK_ROUTES = {
27
+ END_ACTIVITY: "/integrations/timeback/end-activity",
28
+ GET_XP: "/integrations/timeback/xp",
29
+ GET_MASTERY: "/integrations/timeback/mastery",
30
+ GET_HIGHEST_GRADE_MASTERED: "/integrations/timeback/highest-grade-mastered",
31
+ HEARTBEAT: "/integrations/timeback/heartbeat",
32
+ ADVANCE_COURSE: "/integrations/timeback/advance-course",
33
+ UNENROLL_COURSE: "/integrations/timeback/unenroll-course"
34
+ };
35
+ var TIMEBACK_GAME_METRIC_DECIMAL_PLACES = {
36
+ xp: 1,
37
+ mastery: 0,
38
+ score: 2
39
+ };
40
+ var TIMEBACK_GAME_METRIC_COMPARISON_TOLERANCE = {
41
+ xp: 0.5 / 10 ** TIMEBACK_GAME_METRIC_DECIMAL_PLACES.xp,
42
+ mastery: 0,
43
+ time: 60,
44
+ score: 0.5 / 10 ** TIMEBACK_GAME_METRIC_DECIMAL_PLACES.score
45
+ };
46
+
47
+ // ../constants/src/cloudflare.ts
48
+ var CLOUDFLARE_COMPATIBILITY_DATE = "2025-10-11";
49
+
50
+ // ../edge-play/src/constants.ts
51
+ var ASSET_PREFIX = "_assets/";
52
+ var MANIFEST_KEY = "_manifest.json";
53
+ var ASSET_ROUTE_PREFIX = "/api/assets/";
54
+ var ROUTES = {
55
+ /** Route index (lists available routes) */
56
+ INDEX: "/api",
57
+ /** Health check endpoint */
58
+ HEALTH: "/api/health",
59
+ /**
60
+ * Built-in content-addressable asset serving.
61
+ *
62
+ * Serves files from the game's BUCKET (R2) binding under their
63
+ * content-hashed keys, with immutable caching. Registered in the
64
+ * terminal tier so a game's own `server/api/assets/[...path].ts`
65
+ * route supersedes it.
66
+ */
67
+ ASSETS: `${ASSET_ROUTE_PREFIX}*`,
68
+ /** TimeBack integration routes */
69
+ TIMEBACK: {
70
+ END_ACTIVITY: `/api${TIMEBACK_ROUTES.END_ACTIVITY}`,
71
+ GET_XP: `/api${TIMEBACK_ROUTES.GET_XP}`,
72
+ GET_MASTERY: `/api${TIMEBACK_ROUTES.GET_MASTERY}`,
73
+ GET_HIGHEST_GRADE_MASTERED: `/api${TIMEBACK_ROUTES.GET_HIGHEST_GRADE_MASTERED}`,
74
+ HEARTBEAT: `/api${TIMEBACK_ROUTES.HEARTBEAT}`,
75
+ ADVANCE_COURSE: `/api${TIMEBACK_ROUTES.ADVANCE_COURSE}`,
76
+ UNENROLL_COURSE: `/api${TIMEBACK_ROUTES.UNENROLL_COURSE}`
77
+ }
78
+ };
79
+
80
+ // src/lib/bundle/backend.ts
81
+ import { join as join6 } from "node:path";
82
+
83
+ // src/constants/api.ts
84
+ import { join as join2 } from "node:path";
85
+
86
+ // src/constants/server.ts
87
+ import { join } from "node:path";
88
+ var SERVER_ROOT_DIRECTORY = "server";
89
+ var SERVER_LIB_DIRECTORY = join(SERVER_ROOT_DIRECTORY, "lib");
90
+
91
+ // src/constants/api.ts
92
+ var DEFAULT_API_ROUTES_DIRECTORY = join2(SERVER_ROOT_DIRECTORY, "api");
93
+
94
+ // ../better-auth/package.json
95
+ var package_default = {
96
+ name: "@playcademy/better-auth",
97
+ version: "0.0.17-beta.6",
98
+ type: "module",
99
+ exports: {
100
+ "./server": {
101
+ types: "./dist/server.d.ts",
102
+ import: "./dist/server.js",
103
+ require: "./dist/server.js"
104
+ },
105
+ "./client": {
106
+ types: "./dist/client.d.ts",
107
+ import: "./dist/client.js",
108
+ require: "./dist/client.js"
109
+ }
110
+ },
111
+ main: "dist/index.js",
112
+ module: "dist/index.js",
113
+ files: [
114
+ "dist"
115
+ ],
116
+ scripts: {
117
+ build: "bun build.ts"
118
+ },
119
+ dependencies: {
120
+ "@better-auth/utils": "^0.3.0",
121
+ "@better-fetch/fetch": "^1.1.18",
122
+ "@playcademy/sdk": "workspace:*",
123
+ "better-auth": "1.3.33",
124
+ "better-call": "^1.0.19"
125
+ },
126
+ devDependencies: {
127
+ "@inquirer/prompts": "^7.9.0",
128
+ "@playcademy/utils": "workspace:*",
129
+ "@types/bun": "1.3.5",
130
+ typescript: "^5.7.2"
131
+ },
132
+ peerDependencies: {
133
+ typescript: "^5"
134
+ }
135
+ };
136
+
137
+ // src/constants/auth.ts
138
+ var BETTER_AUTH_VERSION = package_default.dependencies["better-auth"];
139
+
140
+ // src/constants/config.ts
141
+ var ENV_FILES = [
142
+ ".env",
143
+ // Loaded first
144
+ ".env.development",
145
+ // Overrides .env
146
+ ".env.local"
147
+ // Overrides all (highest priority)
148
+ ];
149
+
150
+ // src/constants/bucket.ts
151
+ var BUCKET_ALWAYS_SKIP = [".git", ".DS_Store", ".gitignore", ...ENV_FILES];
152
+ var PRESIGNED_UPLOAD_THRESHOLD = 4 * 1024 * 1024;
153
+
154
+ // src/constants/cloudflare.ts
155
+ var CLOUDFLARE_BINDINGS = {
156
+ /** R2 bucket binding name */
157
+ BUCKET: "BUCKET",
158
+ /** KV namespace binding name */
159
+ KV: "KV",
160
+ /** D1 database binding name */
161
+ DB: "DB"
162
+ };
163
+
164
+ // src/constants/database.ts
165
+ import { join as join3 } from "path";
166
+ var DEFAULT_DATABASE_DIRECTORY = join3("server", "db");
167
+
168
+ // src/constants/godot.ts
169
+ import { join as join4 } from "node:path";
170
+ var GODOT_BUILD_DIRECTORIES = {
171
+ /** Root build directory (cleared before each export) */
172
+ ROOT: "build",
173
+ /** Web export subdirectory */
174
+ WEB: join4("build", "web")
175
+ };
176
+ var GODOT_BUILD_OUTPUTS = {
177
+ /** Exported web build entry point */
178
+ INDEX_HTML: join4("build", "web", "index.html"),
179
+ /** Packaged zip file (created by Godot export) */
180
+ ZIP: join4("build", "web_playcademy.zip")
181
+ };
182
+
183
+ // src/constants/paths.ts
184
+ import { homedir } from "node:os";
185
+ import { join as join5 } from "node:path";
186
+ var WORKSPACE_NAME = ".playcademy";
187
+ var CLI_DIRECTORIES = {
188
+ WORKSPACE: WORKSPACE_NAME,
189
+ DATABASE: join5(WORKSPACE_NAME, "db"),
190
+ KV: join5(WORKSPACE_NAME, "kv"),
191
+ BUCKET: join5(WORKSPACE_NAME, "bucket")
192
+ };
193
+ var PLAYCADEMY_HOME = join5(homedir(), ".playcademy");
194
+ var CLI_USER_DIRECTORIES = {
195
+ CONFIG: PLAYCADEMY_HOME,
196
+ BIN: join5(PLAYCADEMY_HOME, "bin"),
197
+ CACHE: join5(PLAYCADEMY_HOME, "cache")
198
+ };
199
+ var CLI_DEFAULT_OUTPUTS = {
200
+ WORKER_BUNDLE: join5(WORKSPACE_NAME, "worker-bundle.js")
201
+ };
202
+
203
+ // src/lib/core/client.ts
204
+ import { PlaycademyClient } from "@playcademy/sdk/internal";
205
+
206
+ // src/lib/core/context.ts
207
+ var context = {};
208
+ function getWorkspace() {
209
+ return context.workspace || process.cwd();
210
+ }
211
+
212
+ // src/lib/core/logger.ts
213
+ import {
214
+ blue,
215
+ blueBright,
216
+ bold as bold2,
217
+ cyan,
218
+ dim as dim2,
219
+ gray,
220
+ green,
221
+ greenBright,
222
+ red,
223
+ underline,
224
+ yellow,
225
+ yellowBright
226
+ } from "colorette";
227
+ import { colorize } from "json-colorizer";
228
+
229
+ // src/lib/core/error.ts
230
+ import { bold, dim, redBright } from "colorette";
231
+ import { ApiError, extractApiErrorInfo } from "@playcademy/sdk/internal";
232
+
233
+ // src/lib/core/errors.ts
234
+ import { ApiError as ApiError2 } from "@playcademy/sdk/internal";
235
+
236
+ // ../utils/src/ansi.ts
237
+ var isInteractive = typeof process !== "undefined" && process.stdout?.isTTY && !process.env.CI && process.env.TERM !== "dumb";
238
+
239
+ // ../utils/src/spinner.ts
240
+ var SPINNER_FRAMES = [
241
+ 10251,
242
+ 10265,
243
+ 10297,
244
+ 10296,
245
+ 10300,
246
+ 10292,
247
+ 10278,
248
+ 10279,
249
+ 10247,
250
+ 10255
251
+ ].map((code) => String.fromCodePoint(code));
252
+ var CHECK_MARK = String.fromCodePoint(10004);
253
+ var CROSS_MARK = String.fromCodePoint(10006);
254
+ var CANCEL_MARK = String.fromCodePoint(9675);
255
+
256
+ // ../utils/src/pure/index.ts
257
+ init_package_json();
258
+
259
+ // src/lib/secrets/env.ts
260
+ init_file_loader();
261
+
262
+ // src/lib/config/loader.ts
263
+ init_file_loader();
264
+
265
+ // ../utils/src/timeback.ts
266
+ var TIMEBACK_DISCREPANCY_QUEUE_WINDOWS = [
267
+ "today",
268
+ "yesterday",
269
+ "this-week",
270
+ "last-week",
271
+ "all",
272
+ "custom"
273
+ ];
274
+ var TIMEBACK_DISCREPANCY_QUEUE_METRICS = ["xp", "mastery", "time", "score"];
275
+ var TIMEBACK_DISCREPANCY_QUEUE_WINDOW_VALUES = new Set(TIMEBACK_DISCREPANCY_QUEUE_WINDOWS);
276
+ var TIMEBACK_DISCREPANCY_QUEUE_METRIC_VALUES = new Set(TIMEBACK_DISCREPANCY_QUEUE_METRICS);
277
+
278
+ // src/lib/core/import.ts
279
+ import * as esbuild from "esbuild";
280
+
281
+ // src/lib/core/transpile.ts
282
+ import * as esbuild2 from "esbuild";
283
+
284
+ // src/lib/bundle/backend.ts
285
+ function getCustomRoutesDirectory(projectPath, config) {
286
+ const customRoutes = config?.integrations?.customRoutes;
287
+ const customRoutesDir = typeof customRoutes === "object" && customRoutes.directory || DEFAULT_API_ROUTES_DIRECTORY;
288
+ return join6(projectPath, customRoutesDir);
289
+ }
290
+
291
+ // src/lib/bucket/local.ts
292
+ import { join as join7 } from "path";
293
+ import { Miniflare } from "miniflare";
294
+ async function openLocalBucket() {
295
+ const bucketDir = join7(getWorkspace(), CLI_DIRECTORIES.BUCKET);
296
+ const mf = new Miniflare({
297
+ modules: [{ type: "ESModule", path: "index.mjs", contents: "" }],
298
+ r2Buckets: [CLOUDFLARE_BINDINGS.BUCKET],
299
+ r2Persist: bucketDir,
300
+ compatibilityDate: CLOUDFLARE_COMPATIBILITY_DATE
301
+ });
302
+ return {
303
+ bucket: await mf.getR2Bucket(CLOUDFLARE_BINDINGS.BUCKET),
304
+ dispose: () => mf.dispose()
305
+ };
306
+ }
307
+
308
+ // src/lib/bucket/dev.ts
309
+ var ASSET_DEV_ROUTE_PREFIX = ASSET_ROUTE_PREFIX;
310
+ function hasCustomAssetsRoute(projectRoot, config) {
311
+ const routesDir = getCustomRoutesDirectory(projectRoot, config);
312
+ return existsSync(join8(routesDir, "assets")) || existsSync(join8(routesDir, "assets.ts"));
313
+ }
314
+ function openLocalAssetBucket() {
315
+ return openLocalBucket();
316
+ }
317
+ async function readLocalManifestScript(bucket) {
318
+ const object = await bucket.get(`${ASSET_PREFIX}${MANIFEST_KEY}`);
319
+ if (!object) {
320
+ return null;
321
+ }
322
+ const manifest = await object.text();
323
+ const payload = `Object.assign({base:${JSON.stringify(ASSET_ROUTE_PREFIX)}},${manifest})`;
324
+ return `<script>self.__PLAYCADEMY_ASSET_MANIFEST=${payload}</script>`;
325
+ }
326
+ async function readLocalAsset(bucket, servingKey) {
327
+ const object = await bucket.get(`${ASSET_PREFIX}${servingKey}`);
328
+ if (!object) {
329
+ return null;
330
+ }
331
+ const body = new Uint8Array(await object.arrayBuffer());
332
+ const contentType = object.httpMetadata?.contentType ?? "application/octet-stream";
333
+ return { body, contentType };
334
+ }
335
+ export {
336
+ ASSET_DEV_ROUTE_PREFIX,
337
+ hasCustomAssetsRoute,
338
+ openLocalAssetBucket,
339
+ readLocalAsset,
340
+ readLocalManifestScript
341
+ };