@stardeck-customer-apps/testing 0.13.0 → 0.15.0

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.
package/SKILL.md CHANGED
@@ -295,6 +295,32 @@ expect(app.edge.latestDisplay()?.action).toBe("show");
295
295
  - `allowNetwork` — permit non-Stardeck outbound fetches (off by default).
296
296
  - `app.reset()` — wipe + re-apply schema + re-seed + clear inbox/sessions.
297
297
  Call in `beforeEach`.
298
+ - `app.reset({ mode: "data" })` — available in testing SDK **0.14.0+**. Faster
299
+ opt-in for stable schemas: truncate ordinary tables in `public`, restart their
300
+ owned serial/identity sequences, re-run the data-only seed, and clear the same
301
+ simulator/user/session state as the default. It preserves tables and other
302
+ schema objects instead of rebuilding them. The default remains full schema
303
+ reset; `app.reset({ mode: "schema" })` explicitly selects that default.
304
+
305
+ ```ts
306
+ // Only after verifying this suite keeps its schema fixed and uses a data-only seed:
307
+ beforeEach(() => app.reset({ mode: "data" }));
308
+ ```
309
+
310
+ Keep `app.reset()` for tests that change schema or depend on INSERTs in the
311
+ schema script. Also keep full reset for independent sequences, materialized
312
+ views, partitioned/foreign tables, temporary tables, and custom triggers or
313
+ cross-schema dependencies. Data mode does not restore those states or detect
314
+ eligibility; inspect both the tests and seed before opting in. All required
315
+ baseline rows must come from the data-only seed callback, not schema SQL.
316
+ Data mode refuses foreign keys requiring a cross-schema cascade. Database and
317
+ seed errors reject the reset; fix them before running the next test.
318
+
319
+ Do not use `it.concurrent` or `test.concurrent` with a shared test app: user,
320
+ database, and simulator state are shared within the file. Keep file isolation
321
+ enabled and let Vitest parallelize files. This option does not change workers
322
+ or the platform's test-run deadline.
323
+
298
324
  - `app.asUser(partial?)` — sign in a test user; `getSession()` in app code
299
325
  resolves to it. `app.signOut()` clears.
300
326
  - `app.issueSession(partial?)` — mint access/refresh tokens for cookie-based
@@ -425,6 +451,14 @@ cursor, limit })`; `list()` intentionally keeps its previous array shape.
425
451
  display, test print, `getCapabilities()`) — captured in `app.edge`; seed
426
452
  devices/peripherals for pairing pickers via `app.edge.seedDevices()` /
427
453
  `seedPeripherals()`.
454
+ - `createStarlensClient().extract()` from the starlens SDK — there is no model in
455
+ tests: register what Starlens answers with `app.starlens.mock({ output, issues })`
456
+ (a successful extraction; each issue path comes back `null` in `output`, as in
457
+ production) or `app.starlens.reject("not a receipt")` (the 422 the SDK maps to
458
+ `UnsupportedDocumentError`). An unregistered call fails with a descriptive 502 so a
459
+ test never passes on an invented extraction. `app.starlens.calls` records each
460
+ request's version, input names/media types (never bytes), schema and
461
+ instructions — assert your `.describe()` aliases reached the wire there.
428
462
 
429
463
  ### Receipt layout and printer capabilities
430
464
 
package/dist/index.d.mts CHANGED
@@ -462,6 +462,44 @@ interface TestEdge {
462
462
  clear(): void;
463
463
  get count(): number;
464
464
  }
465
+ /** One Starlens request the app made, as the simulator saw it (names only, never bytes). */
466
+ interface CapturedStarlensCall {
467
+ version: string;
468
+ inputs: {
469
+ name: string;
470
+ mediaType: string | null;
471
+ source: "url" | "data";
472
+ }[];
473
+ schema: Record<string, unknown>;
474
+ instructions?: string;
475
+ }
476
+ /**
477
+ * What the simulated Starlens answers next. `{ output, issues }` is returned as a
478
+ * successful extraction with every issue path nulled in `output`, exactly as the control
479
+ * plane does; `{ reject }` becomes the 422 `unsupported_document` refusal the SDK maps to
480
+ * `UnsupportedDocumentError`.
481
+ */
482
+ type StarlensReply = {
483
+ output: unknown;
484
+ issues?: {
485
+ path: string;
486
+ reason: string;
487
+ }[];
488
+ } | {
489
+ reject: string;
490
+ };
491
+ interface TestStarlens {
492
+ /** Queue the reply for the next `extract()` call. */
493
+ mock(reply: StarlensReply): void;
494
+ /** Queue a refusal for the next `extract()` call. */
495
+ reject(reason: string): void;
496
+ /** Reply used whenever the queue is empty (null = fail with a descriptive 502). */
497
+ mockDefault(reply: StarlensReply | null): void;
498
+ get calls(): CapturedStarlensCall[];
499
+ latestCall(): CapturedStarlensCall | undefined;
500
+ clear(): void;
501
+ get count(): number;
502
+ }
465
503
  interface TestAppOptions {
466
504
  /** Data stores exposed through the production-shaped STARDECK_DATA_STORES manifest. */
467
505
  dataStores?: TestDataStoreInput[];
@@ -506,6 +544,8 @@ interface TestApp {
506
544
  messages: TestMessages;
507
545
  /** Captured edge print/display ops and peripheral bindings from edge-sdk. */
508
546
  edge: TestEdge;
547
+ /** Scripted replies and captured calls for the Starlens SDK. */
548
+ starlens: TestStarlens;
509
549
  /** Convenience for raw SQL: `app.query("SELECT ...", [param])`. */
510
550
  query<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<T[]>;
511
551
  /**
@@ -520,8 +560,15 @@ interface TestApp {
520
560
  * control-plane verify endpoint resolves these tokens back to the user.
521
561
  */
522
562
  issueSession(user?: Partial<TestUser>): SessionTokens;
523
- /** Drop all tables, re-apply the schema, re-run the seed, clear inbox/user. */
524
- reset(): Promise<void>;
563
+ /**
564
+ * Rebuild the schema (default), re-run the seed, and clear simulator/user state.
565
+ * `mode: "data"` instead truncates ordinary public tables and restarts owned
566
+ * sequences. Use only with a stable schema and data-only seed; it does not
567
+ * restore DDL changes, schema-script data, or independent sequences.
568
+ */
569
+ reset(options?: {
570
+ mode: "schema" | "data";
571
+ }): Promise<void>;
525
572
  /** Tear down the app and restore global fetch. */
526
573
  close(): Promise<void>;
527
574
  }
@@ -564,4 +611,4 @@ declare const TEST_ENV_DEFAULTS: {
564
611
  /** Default location of the DDL snapshot written by `generate-types`. */
565
612
  declare const DEFAULT_SCHEMA_PATH = "./src/generated/data-store-schema.sql";
566
613
 
567
- export { type BindingInfo, CONTROL_PLANE_TEST_URL, type CallRouteOptions, type CapturedCheckout, type CapturedDisplay, type CapturedEmail, type CapturedIdentity, type CapturedIdentityLink, type CapturedMessage, type CapturedPrint, type CapturedTestPrint, type CapturedUpload, DATA_STORE_TEST_HOST, DATA_STORE_TEST_URL, DEFAULT_SCHEMA_PATH, type DeviceInfo, type PaymentSimulatorFailure, type PaymentSimulatorOperation, type PeripheralInfo, STORAGE_TEST_HOST, STORAGE_TEST_URL, type SessionTokens, type SimBoltCharge, type SimBoltConnection, type SimBoltIntentRecord, type SimBoltIntentStatus, TEST_ENV_DEFAULTS, type TestApp, type TestAppOptions, TestDataStoreInput, type TestDirectory, type TestEdge, type TestInbox, type TestMessages, type TestPayments, type TestStorage, type TestUser, WORKFLOW_NAME_PREFIX, callRoute, createTestApp, describeWorkflow, parseWorkflowName };
614
+ export { type BindingInfo, CONTROL_PLANE_TEST_URL, type CallRouteOptions, type CapturedCheckout, type CapturedDisplay, type CapturedEmail, type CapturedIdentity, type CapturedIdentityLink, type CapturedMessage, type CapturedPrint, type CapturedStarlensCall, type CapturedTestPrint, type CapturedUpload, DATA_STORE_TEST_HOST, DATA_STORE_TEST_URL, DEFAULT_SCHEMA_PATH, type DeviceInfo, type PaymentSimulatorFailure, type PaymentSimulatorOperation, type PeripheralInfo, STORAGE_TEST_HOST, STORAGE_TEST_URL, type SessionTokens, type SimBoltCharge, type SimBoltConnection, type SimBoltIntentRecord, type SimBoltIntentStatus, type StarlensReply, TEST_ENV_DEFAULTS, type TestApp, type TestAppOptions, TestDataStoreInput, type TestDirectory, type TestEdge, type TestInbox, type TestMessages, type TestPayments, type TestStarlens, type TestStorage, type TestUser, WORKFLOW_NAME_PREFIX, callRoute, createTestApp, describeWorkflow, parseWorkflowName };
package/dist/index.d.ts CHANGED
@@ -462,6 +462,44 @@ interface TestEdge {
462
462
  clear(): void;
463
463
  get count(): number;
464
464
  }
465
+ /** One Starlens request the app made, as the simulator saw it (names only, never bytes). */
466
+ interface CapturedStarlensCall {
467
+ version: string;
468
+ inputs: {
469
+ name: string;
470
+ mediaType: string | null;
471
+ source: "url" | "data";
472
+ }[];
473
+ schema: Record<string, unknown>;
474
+ instructions?: string;
475
+ }
476
+ /**
477
+ * What the simulated Starlens answers next. `{ output, issues }` is returned as a
478
+ * successful extraction with every issue path nulled in `output`, exactly as the control
479
+ * plane does; `{ reject }` becomes the 422 `unsupported_document` refusal the SDK maps to
480
+ * `UnsupportedDocumentError`.
481
+ */
482
+ type StarlensReply = {
483
+ output: unknown;
484
+ issues?: {
485
+ path: string;
486
+ reason: string;
487
+ }[];
488
+ } | {
489
+ reject: string;
490
+ };
491
+ interface TestStarlens {
492
+ /** Queue the reply for the next `extract()` call. */
493
+ mock(reply: StarlensReply): void;
494
+ /** Queue a refusal for the next `extract()` call. */
495
+ reject(reason: string): void;
496
+ /** Reply used whenever the queue is empty (null = fail with a descriptive 502). */
497
+ mockDefault(reply: StarlensReply | null): void;
498
+ get calls(): CapturedStarlensCall[];
499
+ latestCall(): CapturedStarlensCall | undefined;
500
+ clear(): void;
501
+ get count(): number;
502
+ }
465
503
  interface TestAppOptions {
466
504
  /** Data stores exposed through the production-shaped STARDECK_DATA_STORES manifest. */
467
505
  dataStores?: TestDataStoreInput[];
@@ -506,6 +544,8 @@ interface TestApp {
506
544
  messages: TestMessages;
507
545
  /** Captured edge print/display ops and peripheral bindings from edge-sdk. */
508
546
  edge: TestEdge;
547
+ /** Scripted replies and captured calls for the Starlens SDK. */
548
+ starlens: TestStarlens;
509
549
  /** Convenience for raw SQL: `app.query("SELECT ...", [param])`. */
510
550
  query<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<T[]>;
511
551
  /**
@@ -520,8 +560,15 @@ interface TestApp {
520
560
  * control-plane verify endpoint resolves these tokens back to the user.
521
561
  */
522
562
  issueSession(user?: Partial<TestUser>): SessionTokens;
523
- /** Drop all tables, re-apply the schema, re-run the seed, clear inbox/user. */
524
- reset(): Promise<void>;
563
+ /**
564
+ * Rebuild the schema (default), re-run the seed, and clear simulator/user state.
565
+ * `mode: "data"` instead truncates ordinary public tables and restarts owned
566
+ * sequences. Use only with a stable schema and data-only seed; it does not
567
+ * restore DDL changes, schema-script data, or independent sequences.
568
+ */
569
+ reset(options?: {
570
+ mode: "schema" | "data";
571
+ }): Promise<void>;
525
572
  /** Tear down the app and restore global fetch. */
526
573
  close(): Promise<void>;
527
574
  }
@@ -564,4 +611,4 @@ declare const TEST_ENV_DEFAULTS: {
564
611
  /** Default location of the DDL snapshot written by `generate-types`. */
565
612
  declare const DEFAULT_SCHEMA_PATH = "./src/generated/data-store-schema.sql";
566
613
 
567
- export { type BindingInfo, CONTROL_PLANE_TEST_URL, type CallRouteOptions, type CapturedCheckout, type CapturedDisplay, type CapturedEmail, type CapturedIdentity, type CapturedIdentityLink, type CapturedMessage, type CapturedPrint, type CapturedTestPrint, type CapturedUpload, DATA_STORE_TEST_HOST, DATA_STORE_TEST_URL, DEFAULT_SCHEMA_PATH, type DeviceInfo, type PaymentSimulatorFailure, type PaymentSimulatorOperation, type PeripheralInfo, STORAGE_TEST_HOST, STORAGE_TEST_URL, type SessionTokens, type SimBoltCharge, type SimBoltConnection, type SimBoltIntentRecord, type SimBoltIntentStatus, TEST_ENV_DEFAULTS, type TestApp, type TestAppOptions, TestDataStoreInput, type TestDirectory, type TestEdge, type TestInbox, type TestMessages, type TestPayments, type TestStorage, type TestUser, WORKFLOW_NAME_PREFIX, callRoute, createTestApp, describeWorkflow, parseWorkflowName };
614
+ export { type BindingInfo, CONTROL_PLANE_TEST_URL, type CallRouteOptions, type CapturedCheckout, type CapturedDisplay, type CapturedEmail, type CapturedIdentity, type CapturedIdentityLink, type CapturedMessage, type CapturedPrint, type CapturedStarlensCall, type CapturedTestPrint, type CapturedUpload, DATA_STORE_TEST_HOST, DATA_STORE_TEST_URL, DEFAULT_SCHEMA_PATH, type DeviceInfo, type PaymentSimulatorFailure, type PaymentSimulatorOperation, type PeripheralInfo, STORAGE_TEST_HOST, STORAGE_TEST_URL, type SessionTokens, type SimBoltCharge, type SimBoltConnection, type SimBoltIntentRecord, type SimBoltIntentStatus, type StarlensReply, TEST_ENV_DEFAULTS, type TestApp, type TestAppOptions, TestDataStoreInput, type TestDirectory, type TestEdge, type TestInbox, type TestMessages, type TestPayments, type TestStarlens, type TestStorage, type TestUser, WORKFLOW_NAME_PREFIX, callRoute, createTestApp, describeWorkflow, parseWorkflowName };
package/dist/index.js CHANGED
@@ -73,6 +73,19 @@ async function applySchema(db, schemaSql) {
73
73
  async function dropAllTables(db) {
74
74
  await db.exec(`DROP SCHEMA public CASCADE; CREATE SCHEMA public;`);
75
75
  }
76
+ async function listPublicTables(db) {
77
+ const { rows } = await db.query(`
78
+ SELECT format('%I.%I', n.nspname, c.relname) AS name
79
+ FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
80
+ WHERE n.nspname = 'public' AND c.relkind = 'r'
81
+ ORDER BY c.relname
82
+ `);
83
+ return rows.map(({ name }) => name);
84
+ }
85
+ async function truncateAllTables(db) {
86
+ const tables = await listPublicTables(db);
87
+ if (tables.length) await db.exec(`TRUNCATE TABLE ${tables.join(", ")} RESTART IDENTITY`);
88
+ }
76
89
  var identityParsers = null;
77
90
  function getIdentityParsers() {
78
91
  if (!identityParsers) {
@@ -131,6 +144,9 @@ var state = globalSingleton("state", () => ({
131
144
  edgeBindings: /* @__PURE__ */ new Map(),
132
145
  edgeCapabilities: null,
133
146
  edgePeripheralCapabilities: /* @__PURE__ */ new Map(),
147
+ starlensCalls: [],
148
+ starlensReplies: [],
149
+ starlensDefaultReply: null,
134
150
  allowNetwork: false
135
151
  }));
136
152
  function requireDb() {
@@ -2540,6 +2556,104 @@ function createEdge() {
2540
2556
  };
2541
2557
  }
2542
2558
 
2559
+ // src/simulator/starlens.ts
2560
+ function starlensFailure(error, status, code) {
2561
+ return json({ success: false, error, code }, status);
2562
+ }
2563
+ function nullFlagged(output, issues) {
2564
+ const copy = JSON.parse(JSON.stringify(output ?? null));
2565
+ for (const { path } of issues) {
2566
+ const keys = path.replace(/^output\./, "").replace(/\[(\d+)\]/g, ".$1").split(".");
2567
+ const last = keys.pop();
2568
+ const parent = keys.reduce(
2569
+ (o, k) => o != null && typeof o === "object" ? o[k] : void 0,
2570
+ copy
2571
+ );
2572
+ if (parent != null && typeof parent === "object" && Object.prototype.hasOwnProperty.call(parent, last)) {
2573
+ parent[last] = null;
2574
+ }
2575
+ }
2576
+ return copy;
2577
+ }
2578
+ async function handleStarlensExtract(request, signedDeploymentId) {
2579
+ const body = await readJsonBody(request);
2580
+ if (signedDeploymentId !== null && body.deploymentId !== signedDeploymentId) {
2581
+ return starlensFailure("Deployment ID mismatch", 401, "AUTHENTICATION_ERROR");
2582
+ }
2583
+ const inputs = Array.isArray(body.inputs) ? body.inputs : [];
2584
+ if (inputs.length === 0) {
2585
+ return starlensFailure(
2586
+ "Validation failed \u2014 inputs: at least one input",
2587
+ 400,
2588
+ "VALIDATION_ERROR"
2589
+ );
2590
+ }
2591
+ const schema = body.schema;
2592
+ if (!schema || schema.type !== "object" || typeof schema.properties !== "object") {
2593
+ return starlensFailure(
2594
+ "Validation failed \u2014 schema.properties is required",
2595
+ 400,
2596
+ "VALIDATION_ERROR"
2597
+ );
2598
+ }
2599
+ const call = {
2600
+ version: typeof body.version === "string" ? body.version : "latest",
2601
+ inputs: inputs.map((i) => ({
2602
+ name: typeof i.filename === "string" ? i.filename : typeof i.url === "string" ? i.url : "upload",
2603
+ mediaType: typeof i.mediaType === "string" ? i.mediaType : null,
2604
+ source: typeof i.url === "string" ? "url" : "data"
2605
+ })),
2606
+ schema,
2607
+ instructions: typeof body.instructions === "string" ? body.instructions : void 0
2608
+ };
2609
+ state.starlensCalls.push(call);
2610
+ const reply = state.starlensReplies.shift() ?? state.starlensDefaultReply;
2611
+ if (!reply) {
2612
+ return starlensFailure(
2613
+ "[stardeck-testing] No Starlens reply registered. Call app.starlens.mock({ output, issues }) or app.starlens.reject(reason) before the request.",
2614
+ 502,
2615
+ "model_failed"
2616
+ );
2617
+ }
2618
+ if ("reject" in reply) {
2619
+ return starlensFailure(reply.reject, 422, "unsupported_document");
2620
+ }
2621
+ const issues = reply.issues ?? [];
2622
+ return success({
2623
+ version: call.version === "latest" ? "v1" : call.version,
2624
+ output: nullFlagged(reply.output, issues),
2625
+ issues,
2626
+ usage: { inputTokens: 0, outputTokens: 0, cost: 0 }
2627
+ });
2628
+ }
2629
+ function createStarlens() {
2630
+ return {
2631
+ mock(reply) {
2632
+ state.starlensReplies.push(reply);
2633
+ },
2634
+ reject(reason) {
2635
+ state.starlensReplies.push({ reject: reason });
2636
+ },
2637
+ mockDefault(reply) {
2638
+ state.starlensDefaultReply = reply;
2639
+ },
2640
+ get calls() {
2641
+ return state.starlensCalls;
2642
+ },
2643
+ latestCall() {
2644
+ return state.starlensCalls[state.starlensCalls.length - 1];
2645
+ },
2646
+ clear() {
2647
+ state.starlensCalls = [];
2648
+ state.starlensReplies = [];
2649
+ state.starlensDefaultReply = null;
2650
+ },
2651
+ get count() {
2652
+ return state.starlensCalls.length;
2653
+ }
2654
+ };
2655
+ }
2656
+
2543
2657
  // src/simulator/router.ts
2544
2658
  var fetchHolder = globalSingleton("fetch-holder", () => ({
2545
2659
  originalFetch: null
@@ -2561,7 +2675,8 @@ function requiresDeploymentHmac(request, url) {
2561
2675
  /^\/api\/deployments\/[^/]+\/integrations\/(slack|line|facebook)(\/.*)?$/
2562
2676
  );
2563
2677
  const edgeMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/edge(\/.*)?$/);
2564
- return !!(dataStoreMatch || isEmail || identitiesMatch || storeMatch || messagingMatch || edgeMatch);
2678
+ const isStarlens = url.pathname === "/api/starlens/extract";
2679
+ return !!(isStarlens || dataStoreMatch || isEmail || identitiesMatch || storeMatch || messagingMatch || edgeMatch);
2565
2680
  }
2566
2681
  function deploymentIdFromPath(url) {
2567
2682
  const match = url.pathname.match(/\/api\/deployments\/([^/]+)(?:\/|$)/);
@@ -2591,6 +2706,7 @@ async function handleSimulatedRequest(request, url) {
2591
2706
  );
2592
2707
  const edgeMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/edge(\/.*)?$/);
2593
2708
  const isStorageHost = url.hostname === STORAGE_TEST_HOST;
2709
+ let signedDeploymentId = null;
2594
2710
  if (requiresDeploymentHmac(request, url)) {
2595
2711
  const authHeader = request.headers.get("X-Stardeck-Auth");
2596
2712
  if (!authHeader) {
@@ -2602,6 +2718,7 @@ async function handleSimulatedRequest(request, url) {
2602
2718
  if (!authPayload || pathDeploymentId !== null && authPayload.deploymentId !== pathDeploymentId) {
2603
2719
  return failure("Invalid authentication", 401);
2604
2720
  }
2721
+ signedDeploymentId = authPayload.deploymentId;
2605
2722
  }
2606
2723
  if (isStorageHost) {
2607
2724
  return handleStorageRequest(request, url);
@@ -2622,6 +2739,9 @@ async function handleSimulatedRequest(request, url) {
2622
2739
  if (edgeMatch) {
2623
2740
  return handleEdgeRequest(request, edgeMatch[1] ?? "");
2624
2741
  }
2742
+ if (url.pathname === "/api/starlens/extract" && request.method === "POST") {
2743
+ return handleStarlensExtract(request, signedDeploymentId);
2744
+ }
2625
2745
  if (dataStoreMatch) {
2626
2746
  const subPath = dataStoreMatch[1] ?? "";
2627
2747
  const db = requireDb();
@@ -2643,7 +2763,7 @@ async function handleSimulatedRequest(request, url) {
2643
2763
  }
2644
2764
  }
2645
2765
  return failure(
2646
- `[stardeck-testing] No simulator for ${request.method} ${url.pathname}. Supported: data-store query/mutate/schema, email send, identities CRUD, payments store/checkout, storage upload/files, messaging send, edge print/display/bindings, auth verify/refresh, Neon /sql.`,
2766
+ `[stardeck-testing] No simulator for ${request.method} ${url.pathname}. Supported: data-store query/mutate/schema, email send, identities CRUD, payments store/checkout, storage upload/files, messaging send, edge print/display/bindings, starlens extract, auth verify/refresh, Neon /sql.`,
2647
2767
  404
2648
2768
  );
2649
2769
  }
@@ -2811,6 +2931,7 @@ async function createTestApp(options = {}) {
2811
2931
  const storage = createStorage();
2812
2932
  const messages = createMessages();
2813
2933
  const edge = createEdge();
2934
+ const starlens = createStarlens();
2814
2935
  const app = {
2815
2936
  db,
2816
2937
  inbox,
@@ -2819,6 +2940,7 @@ async function createTestApp(options = {}) {
2819
2940
  storage,
2820
2941
  messages,
2821
2942
  edge,
2943
+ starlens,
2822
2944
  async query(sql, params = []) {
2823
2945
  const result = await db.query(sql, params);
2824
2946
  return result.rows;
@@ -2841,9 +2963,14 @@ async function createTestApp(options = {}) {
2841
2963
  state.refreshSessions.set(tokens.refreshToken, fullUser);
2842
2964
  return tokens;
2843
2965
  },
2844
- async reset() {
2845
- await dropAllTables(db);
2846
- await initialize();
2966
+ async reset(resetOptions) {
2967
+ if (resetOptions?.mode === "data") {
2968
+ await truncateAllTables(db);
2969
+ if (options.seed) await options.seed(db);
2970
+ } else {
2971
+ await dropAllTables(db);
2972
+ await initialize();
2973
+ }
2847
2974
  state.currentUser = null;
2848
2975
  state.sessions.clear();
2849
2976
  state.refreshSessions.clear();
@@ -2855,6 +2982,7 @@ async function createTestApp(options = {}) {
2855
2982
  storage.clear();
2856
2983
  messages.clear();
2857
2984
  edge.clear();
2985
+ starlens.clear();
2858
2986
  },
2859
2987
  async close() {
2860
2988
  state.db = null;
@@ -2869,6 +2997,7 @@ async function createTestApp(options = {}) {
2869
2997
  storage.clear();
2870
2998
  messages.clear();
2871
2999
  edge.clear();
3000
+ starlens.clear();
2872
3001
  uninstallFetchRouter();
2873
3002
  await db.close();
2874
3003
  }
package/dist/index.mjs CHANGED
@@ -24,6 +24,19 @@ async function applySchema(db, schemaSql) {
24
24
  async function dropAllTables(db) {
25
25
  await db.exec(`DROP SCHEMA public CASCADE; CREATE SCHEMA public;`);
26
26
  }
27
+ async function listPublicTables(db) {
28
+ const { rows } = await db.query(`
29
+ SELECT format('%I.%I', n.nspname, c.relname) AS name
30
+ FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
31
+ WHERE n.nspname = 'public' AND c.relkind = 'r'
32
+ ORDER BY c.relname
33
+ `);
34
+ return rows.map(({ name }) => name);
35
+ }
36
+ async function truncateAllTables(db) {
37
+ const tables = await listPublicTables(db);
38
+ if (tables.length) await db.exec(`TRUNCATE TABLE ${tables.join(", ")} RESTART IDENTITY`);
39
+ }
27
40
  var identityParsers = null;
28
41
  function getIdentityParsers() {
29
42
  if (!identityParsers) {
@@ -82,6 +95,9 @@ var state = globalSingleton("state", () => ({
82
95
  edgeBindings: /* @__PURE__ */ new Map(),
83
96
  edgeCapabilities: null,
84
97
  edgePeripheralCapabilities: /* @__PURE__ */ new Map(),
98
+ starlensCalls: [],
99
+ starlensReplies: [],
100
+ starlensDefaultReply: null,
85
101
  allowNetwork: false
86
102
  }));
87
103
  function requireDb() {
@@ -2491,6 +2507,104 @@ function createEdge() {
2491
2507
  };
2492
2508
  }
2493
2509
 
2510
+ // src/simulator/starlens.ts
2511
+ function starlensFailure(error, status, code) {
2512
+ return json({ success: false, error, code }, status);
2513
+ }
2514
+ function nullFlagged(output, issues) {
2515
+ const copy = JSON.parse(JSON.stringify(output ?? null));
2516
+ for (const { path } of issues) {
2517
+ const keys = path.replace(/^output\./, "").replace(/\[(\d+)\]/g, ".$1").split(".");
2518
+ const last = keys.pop();
2519
+ const parent = keys.reduce(
2520
+ (o, k) => o != null && typeof o === "object" ? o[k] : void 0,
2521
+ copy
2522
+ );
2523
+ if (parent != null && typeof parent === "object" && Object.prototype.hasOwnProperty.call(parent, last)) {
2524
+ parent[last] = null;
2525
+ }
2526
+ }
2527
+ return copy;
2528
+ }
2529
+ async function handleStarlensExtract(request, signedDeploymentId) {
2530
+ const body = await readJsonBody(request);
2531
+ if (signedDeploymentId !== null && body.deploymentId !== signedDeploymentId) {
2532
+ return starlensFailure("Deployment ID mismatch", 401, "AUTHENTICATION_ERROR");
2533
+ }
2534
+ const inputs = Array.isArray(body.inputs) ? body.inputs : [];
2535
+ if (inputs.length === 0) {
2536
+ return starlensFailure(
2537
+ "Validation failed \u2014 inputs: at least one input",
2538
+ 400,
2539
+ "VALIDATION_ERROR"
2540
+ );
2541
+ }
2542
+ const schema = body.schema;
2543
+ if (!schema || schema.type !== "object" || typeof schema.properties !== "object") {
2544
+ return starlensFailure(
2545
+ "Validation failed \u2014 schema.properties is required",
2546
+ 400,
2547
+ "VALIDATION_ERROR"
2548
+ );
2549
+ }
2550
+ const call = {
2551
+ version: typeof body.version === "string" ? body.version : "latest",
2552
+ inputs: inputs.map((i) => ({
2553
+ name: typeof i.filename === "string" ? i.filename : typeof i.url === "string" ? i.url : "upload",
2554
+ mediaType: typeof i.mediaType === "string" ? i.mediaType : null,
2555
+ source: typeof i.url === "string" ? "url" : "data"
2556
+ })),
2557
+ schema,
2558
+ instructions: typeof body.instructions === "string" ? body.instructions : void 0
2559
+ };
2560
+ state.starlensCalls.push(call);
2561
+ const reply = state.starlensReplies.shift() ?? state.starlensDefaultReply;
2562
+ if (!reply) {
2563
+ return starlensFailure(
2564
+ "[stardeck-testing] No Starlens reply registered. Call app.starlens.mock({ output, issues }) or app.starlens.reject(reason) before the request.",
2565
+ 502,
2566
+ "model_failed"
2567
+ );
2568
+ }
2569
+ if ("reject" in reply) {
2570
+ return starlensFailure(reply.reject, 422, "unsupported_document");
2571
+ }
2572
+ const issues = reply.issues ?? [];
2573
+ return success({
2574
+ version: call.version === "latest" ? "v1" : call.version,
2575
+ output: nullFlagged(reply.output, issues),
2576
+ issues,
2577
+ usage: { inputTokens: 0, outputTokens: 0, cost: 0 }
2578
+ });
2579
+ }
2580
+ function createStarlens() {
2581
+ return {
2582
+ mock(reply) {
2583
+ state.starlensReplies.push(reply);
2584
+ },
2585
+ reject(reason) {
2586
+ state.starlensReplies.push({ reject: reason });
2587
+ },
2588
+ mockDefault(reply) {
2589
+ state.starlensDefaultReply = reply;
2590
+ },
2591
+ get calls() {
2592
+ return state.starlensCalls;
2593
+ },
2594
+ latestCall() {
2595
+ return state.starlensCalls[state.starlensCalls.length - 1];
2596
+ },
2597
+ clear() {
2598
+ state.starlensCalls = [];
2599
+ state.starlensReplies = [];
2600
+ state.starlensDefaultReply = null;
2601
+ },
2602
+ get count() {
2603
+ return state.starlensCalls.length;
2604
+ }
2605
+ };
2606
+ }
2607
+
2494
2608
  // src/simulator/router.ts
2495
2609
  var fetchHolder = globalSingleton("fetch-holder", () => ({
2496
2610
  originalFetch: null
@@ -2512,7 +2626,8 @@ function requiresDeploymentHmac(request, url) {
2512
2626
  /^\/api\/deployments\/[^/]+\/integrations\/(slack|line|facebook)(\/.*)?$/
2513
2627
  );
2514
2628
  const edgeMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/edge(\/.*)?$/);
2515
- return !!(dataStoreMatch || isEmail || identitiesMatch || storeMatch || messagingMatch || edgeMatch);
2629
+ const isStarlens = url.pathname === "/api/starlens/extract";
2630
+ return !!(isStarlens || dataStoreMatch || isEmail || identitiesMatch || storeMatch || messagingMatch || edgeMatch);
2516
2631
  }
2517
2632
  function deploymentIdFromPath(url) {
2518
2633
  const match = url.pathname.match(/\/api\/deployments\/([^/]+)(?:\/|$)/);
@@ -2542,6 +2657,7 @@ async function handleSimulatedRequest(request, url) {
2542
2657
  );
2543
2658
  const edgeMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/edge(\/.*)?$/);
2544
2659
  const isStorageHost = url.hostname === STORAGE_TEST_HOST;
2660
+ let signedDeploymentId = null;
2545
2661
  if (requiresDeploymentHmac(request, url)) {
2546
2662
  const authHeader = request.headers.get("X-Stardeck-Auth");
2547
2663
  if (!authHeader) {
@@ -2553,6 +2669,7 @@ async function handleSimulatedRequest(request, url) {
2553
2669
  if (!authPayload || pathDeploymentId !== null && authPayload.deploymentId !== pathDeploymentId) {
2554
2670
  return failure("Invalid authentication", 401);
2555
2671
  }
2672
+ signedDeploymentId = authPayload.deploymentId;
2556
2673
  }
2557
2674
  if (isStorageHost) {
2558
2675
  return handleStorageRequest(request, url);
@@ -2573,6 +2690,9 @@ async function handleSimulatedRequest(request, url) {
2573
2690
  if (edgeMatch) {
2574
2691
  return handleEdgeRequest(request, edgeMatch[1] ?? "");
2575
2692
  }
2693
+ if (url.pathname === "/api/starlens/extract" && request.method === "POST") {
2694
+ return handleStarlensExtract(request, signedDeploymentId);
2695
+ }
2576
2696
  if (dataStoreMatch) {
2577
2697
  const subPath = dataStoreMatch[1] ?? "";
2578
2698
  const db = requireDb();
@@ -2594,7 +2714,7 @@ async function handleSimulatedRequest(request, url) {
2594
2714
  }
2595
2715
  }
2596
2716
  return failure(
2597
- `[stardeck-testing] No simulator for ${request.method} ${url.pathname}. Supported: data-store query/mutate/schema, email send, identities CRUD, payments store/checkout, storage upload/files, messaging send, edge print/display/bindings, auth verify/refresh, Neon /sql.`,
2717
+ `[stardeck-testing] No simulator for ${request.method} ${url.pathname}. Supported: data-store query/mutate/schema, email send, identities CRUD, payments store/checkout, storage upload/files, messaging send, edge print/display/bindings, starlens extract, auth verify/refresh, Neon /sql.`,
2598
2718
  404
2599
2719
  );
2600
2720
  }
@@ -2762,6 +2882,7 @@ async function createTestApp(options = {}) {
2762
2882
  const storage = createStorage();
2763
2883
  const messages = createMessages();
2764
2884
  const edge = createEdge();
2885
+ const starlens = createStarlens();
2765
2886
  const app = {
2766
2887
  db,
2767
2888
  inbox,
@@ -2770,6 +2891,7 @@ async function createTestApp(options = {}) {
2770
2891
  storage,
2771
2892
  messages,
2772
2893
  edge,
2894
+ starlens,
2773
2895
  async query(sql, params = []) {
2774
2896
  const result = await db.query(sql, params);
2775
2897
  return result.rows;
@@ -2792,9 +2914,14 @@ async function createTestApp(options = {}) {
2792
2914
  state.refreshSessions.set(tokens.refreshToken, fullUser);
2793
2915
  return tokens;
2794
2916
  },
2795
- async reset() {
2796
- await dropAllTables(db);
2797
- await initialize();
2917
+ async reset(resetOptions) {
2918
+ if (resetOptions?.mode === "data") {
2919
+ await truncateAllTables(db);
2920
+ if (options.seed) await options.seed(db);
2921
+ } else {
2922
+ await dropAllTables(db);
2923
+ await initialize();
2924
+ }
2798
2925
  state.currentUser = null;
2799
2926
  state.sessions.clear();
2800
2927
  state.refreshSessions.clear();
@@ -2806,6 +2933,7 @@ async function createTestApp(options = {}) {
2806
2933
  storage.clear();
2807
2934
  messages.clear();
2808
2935
  edge.clear();
2936
+ starlens.clear();
2809
2937
  },
2810
2938
  async close() {
2811
2939
  state.db = null;
@@ -2820,6 +2948,7 @@ async function createTestApp(options = {}) {
2820
2948
  storage.clear();
2821
2949
  messages.clear();
2822
2950
  edge.clear();
2951
+ starlens.clear();
2823
2952
  uninstallFetchRouter();
2824
2953
  await db.close();
2825
2954
  }
@@ -74,6 +74,9 @@ var state = globalSingleton("state", () => ({
74
74
  edgeBindings: /* @__PURE__ */ new Map(),
75
75
  edgeCapabilities: null,
76
76
  edgePeripheralCapabilities: /* @__PURE__ */ new Map(),
77
+ starlensCalls: [],
78
+ starlensReplies: [],
79
+ starlensDefaultReply: null,
77
80
  allowNetwork: false
78
81
  }));
79
82
 
@@ -47,6 +47,9 @@ var state = globalSingleton("state", () => ({
47
47
  edgeBindings: /* @__PURE__ */ new Map(),
48
48
  edgeCapabilities: null,
49
49
  edgePeripheralCapabilities: /* @__PURE__ */ new Map(),
50
+ starlensCalls: [],
51
+ starlensReplies: [],
52
+ starlensDefaultReply: null,
50
53
  allowNetwork: false
51
54
  }));
52
55
 
package/dist/setup.js CHANGED
@@ -68,6 +68,9 @@ var state = globalSingleton("state", () => ({
68
68
  edgeBindings: /* @__PURE__ */ new Map(),
69
69
  edgeCapabilities: null,
70
70
  edgePeripheralCapabilities: /* @__PURE__ */ new Map(),
71
+ starlensCalls: [],
72
+ starlensReplies: [],
73
+ starlensDefaultReply: null,
71
74
  allowNetwork: false
72
75
  }));
73
76
  function requireDb() {
@@ -2000,6 +2003,77 @@ async function handleEdgeRequest(request, subPath) {
2000
2003
  return edgeFailure(`No edge simulator for ${method} .../edge${subPath}`, 404);
2001
2004
  }
2002
2005
 
2006
+ // src/simulator/starlens.ts
2007
+ function starlensFailure(error, status, code) {
2008
+ return json({ success: false, error, code }, status);
2009
+ }
2010
+ function nullFlagged(output, issues) {
2011
+ const copy = JSON.parse(JSON.stringify(output ?? null));
2012
+ for (const { path } of issues) {
2013
+ const keys = path.replace(/^output\./, "").replace(/\[(\d+)\]/g, ".$1").split(".");
2014
+ const last = keys.pop();
2015
+ const parent = keys.reduce(
2016
+ (o, k) => o != null && typeof o === "object" ? o[k] : void 0,
2017
+ copy
2018
+ );
2019
+ if (parent != null && typeof parent === "object" && Object.prototype.hasOwnProperty.call(parent, last)) {
2020
+ parent[last] = null;
2021
+ }
2022
+ }
2023
+ return copy;
2024
+ }
2025
+ async function handleStarlensExtract(request, signedDeploymentId) {
2026
+ const body = await readJsonBody(request);
2027
+ if (signedDeploymentId !== null && body.deploymentId !== signedDeploymentId) {
2028
+ return starlensFailure("Deployment ID mismatch", 401, "AUTHENTICATION_ERROR");
2029
+ }
2030
+ const inputs = Array.isArray(body.inputs) ? body.inputs : [];
2031
+ if (inputs.length === 0) {
2032
+ return starlensFailure(
2033
+ "Validation failed \u2014 inputs: at least one input",
2034
+ 400,
2035
+ "VALIDATION_ERROR"
2036
+ );
2037
+ }
2038
+ const schema = body.schema;
2039
+ if (!schema || schema.type !== "object" || typeof schema.properties !== "object") {
2040
+ return starlensFailure(
2041
+ "Validation failed \u2014 schema.properties is required",
2042
+ 400,
2043
+ "VALIDATION_ERROR"
2044
+ );
2045
+ }
2046
+ const call = {
2047
+ version: typeof body.version === "string" ? body.version : "latest",
2048
+ inputs: inputs.map((i) => ({
2049
+ name: typeof i.filename === "string" ? i.filename : typeof i.url === "string" ? i.url : "upload",
2050
+ mediaType: typeof i.mediaType === "string" ? i.mediaType : null,
2051
+ source: typeof i.url === "string" ? "url" : "data"
2052
+ })),
2053
+ schema,
2054
+ instructions: typeof body.instructions === "string" ? body.instructions : void 0
2055
+ };
2056
+ state.starlensCalls.push(call);
2057
+ const reply = state.starlensReplies.shift() ?? state.starlensDefaultReply;
2058
+ if (!reply) {
2059
+ return starlensFailure(
2060
+ "[stardeck-testing] No Starlens reply registered. Call app.starlens.mock({ output, issues }) or app.starlens.reject(reason) before the request.",
2061
+ 502,
2062
+ "model_failed"
2063
+ );
2064
+ }
2065
+ if ("reject" in reply) {
2066
+ return starlensFailure(reply.reject, 422, "unsupported_document");
2067
+ }
2068
+ const issues = reply.issues ?? [];
2069
+ return success({
2070
+ version: call.version === "latest" ? "v1" : call.version,
2071
+ output: nullFlagged(reply.output, issues),
2072
+ issues,
2073
+ usage: { inputTokens: 0, outputTokens: 0, cost: 0 }
2074
+ });
2075
+ }
2076
+
2003
2077
  // src/simulator/router.ts
2004
2078
  var fetchHolder = globalSingleton("fetch-holder", () => ({
2005
2079
  originalFetch: null
@@ -2021,7 +2095,8 @@ function requiresDeploymentHmac(request, url) {
2021
2095
  /^\/api\/deployments\/[^/]+\/integrations\/(slack|line|facebook)(\/.*)?$/
2022
2096
  );
2023
2097
  const edgeMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/edge(\/.*)?$/);
2024
- return !!(dataStoreMatch || isEmail || identitiesMatch || storeMatch || messagingMatch || edgeMatch);
2098
+ const isStarlens = url.pathname === "/api/starlens/extract";
2099
+ return !!(isStarlens || dataStoreMatch || isEmail || identitiesMatch || storeMatch || messagingMatch || edgeMatch);
2025
2100
  }
2026
2101
  function deploymentIdFromPath(url) {
2027
2102
  const match = url.pathname.match(/\/api\/deployments\/([^/]+)(?:\/|$)/);
@@ -2051,6 +2126,7 @@ async function handleSimulatedRequest(request, url) {
2051
2126
  );
2052
2127
  const edgeMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/edge(\/.*)?$/);
2053
2128
  const isStorageHost = url.hostname === STORAGE_TEST_HOST;
2129
+ let signedDeploymentId = null;
2054
2130
  if (requiresDeploymentHmac(request, url)) {
2055
2131
  const authHeader = request.headers.get("X-Stardeck-Auth");
2056
2132
  if (!authHeader) {
@@ -2062,6 +2138,7 @@ async function handleSimulatedRequest(request, url) {
2062
2138
  if (!authPayload || pathDeploymentId !== null && authPayload.deploymentId !== pathDeploymentId) {
2063
2139
  return failure("Invalid authentication", 401);
2064
2140
  }
2141
+ signedDeploymentId = authPayload.deploymentId;
2065
2142
  }
2066
2143
  if (isStorageHost) {
2067
2144
  return handleStorageRequest(request, url);
@@ -2082,6 +2159,9 @@ async function handleSimulatedRequest(request, url) {
2082
2159
  if (edgeMatch) {
2083
2160
  return handleEdgeRequest(request, edgeMatch[1] ?? "");
2084
2161
  }
2162
+ if (url.pathname === "/api/starlens/extract" && request.method === "POST") {
2163
+ return handleStarlensExtract(request, signedDeploymentId);
2164
+ }
2085
2165
  if (dataStoreMatch) {
2086
2166
  const subPath = dataStoreMatch[1] ?? "";
2087
2167
  const db = requireDb();
@@ -2103,7 +2183,7 @@ async function handleSimulatedRequest(request, url) {
2103
2183
  }
2104
2184
  }
2105
2185
  return failure(
2106
- `[stardeck-testing] No simulator for ${request.method} ${url.pathname}. Supported: data-store query/mutate/schema, email send, identities CRUD, payments store/checkout, storage upload/files, messaging send, edge print/display/bindings, auth verify/refresh, Neon /sql.`,
2186
+ `[stardeck-testing] No simulator for ${request.method} ${url.pathname}. Supported: data-store query/mutate/schema, email send, identities CRUD, payments store/checkout, storage upload/files, messaging send, edge print/display/bindings, starlens extract, auth verify/refresh, Neon /sql.`,
2107
2187
  404
2108
2188
  );
2109
2189
  }
package/dist/setup.mjs CHANGED
@@ -44,6 +44,9 @@ var state = globalSingleton("state", () => ({
44
44
  edgeBindings: /* @__PURE__ */ new Map(),
45
45
  edgeCapabilities: null,
46
46
  edgePeripheralCapabilities: /* @__PURE__ */ new Map(),
47
+ starlensCalls: [],
48
+ starlensReplies: [],
49
+ starlensDefaultReply: null,
47
50
  allowNetwork: false
48
51
  }));
49
52
  function requireDb() {
@@ -1976,6 +1979,77 @@ async function handleEdgeRequest(request, subPath) {
1976
1979
  return edgeFailure(`No edge simulator for ${method} .../edge${subPath}`, 404);
1977
1980
  }
1978
1981
 
1982
+ // src/simulator/starlens.ts
1983
+ function starlensFailure(error, status, code) {
1984
+ return json({ success: false, error, code }, status);
1985
+ }
1986
+ function nullFlagged(output, issues) {
1987
+ const copy = JSON.parse(JSON.stringify(output ?? null));
1988
+ for (const { path } of issues) {
1989
+ const keys = path.replace(/^output\./, "").replace(/\[(\d+)\]/g, ".$1").split(".");
1990
+ const last = keys.pop();
1991
+ const parent = keys.reduce(
1992
+ (o, k) => o != null && typeof o === "object" ? o[k] : void 0,
1993
+ copy
1994
+ );
1995
+ if (parent != null && typeof parent === "object" && Object.prototype.hasOwnProperty.call(parent, last)) {
1996
+ parent[last] = null;
1997
+ }
1998
+ }
1999
+ return copy;
2000
+ }
2001
+ async function handleStarlensExtract(request, signedDeploymentId) {
2002
+ const body = await readJsonBody(request);
2003
+ if (signedDeploymentId !== null && body.deploymentId !== signedDeploymentId) {
2004
+ return starlensFailure("Deployment ID mismatch", 401, "AUTHENTICATION_ERROR");
2005
+ }
2006
+ const inputs = Array.isArray(body.inputs) ? body.inputs : [];
2007
+ if (inputs.length === 0) {
2008
+ return starlensFailure(
2009
+ "Validation failed \u2014 inputs: at least one input",
2010
+ 400,
2011
+ "VALIDATION_ERROR"
2012
+ );
2013
+ }
2014
+ const schema = body.schema;
2015
+ if (!schema || schema.type !== "object" || typeof schema.properties !== "object") {
2016
+ return starlensFailure(
2017
+ "Validation failed \u2014 schema.properties is required",
2018
+ 400,
2019
+ "VALIDATION_ERROR"
2020
+ );
2021
+ }
2022
+ const call = {
2023
+ version: typeof body.version === "string" ? body.version : "latest",
2024
+ inputs: inputs.map((i) => ({
2025
+ name: typeof i.filename === "string" ? i.filename : typeof i.url === "string" ? i.url : "upload",
2026
+ mediaType: typeof i.mediaType === "string" ? i.mediaType : null,
2027
+ source: typeof i.url === "string" ? "url" : "data"
2028
+ })),
2029
+ schema,
2030
+ instructions: typeof body.instructions === "string" ? body.instructions : void 0
2031
+ };
2032
+ state.starlensCalls.push(call);
2033
+ const reply = state.starlensReplies.shift() ?? state.starlensDefaultReply;
2034
+ if (!reply) {
2035
+ return starlensFailure(
2036
+ "[stardeck-testing] No Starlens reply registered. Call app.starlens.mock({ output, issues }) or app.starlens.reject(reason) before the request.",
2037
+ 502,
2038
+ "model_failed"
2039
+ );
2040
+ }
2041
+ if ("reject" in reply) {
2042
+ return starlensFailure(reply.reject, 422, "unsupported_document");
2043
+ }
2044
+ const issues = reply.issues ?? [];
2045
+ return success({
2046
+ version: call.version === "latest" ? "v1" : call.version,
2047
+ output: nullFlagged(reply.output, issues),
2048
+ issues,
2049
+ usage: { inputTokens: 0, outputTokens: 0, cost: 0 }
2050
+ });
2051
+ }
2052
+
1979
2053
  // src/simulator/router.ts
1980
2054
  var fetchHolder = globalSingleton("fetch-holder", () => ({
1981
2055
  originalFetch: null
@@ -1997,7 +2071,8 @@ function requiresDeploymentHmac(request, url) {
1997
2071
  /^\/api\/deployments\/[^/]+\/integrations\/(slack|line|facebook)(\/.*)?$/
1998
2072
  );
1999
2073
  const edgeMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/edge(\/.*)?$/);
2000
- return !!(dataStoreMatch || isEmail || identitiesMatch || storeMatch || messagingMatch || edgeMatch);
2074
+ const isStarlens = url.pathname === "/api/starlens/extract";
2075
+ return !!(isStarlens || dataStoreMatch || isEmail || identitiesMatch || storeMatch || messagingMatch || edgeMatch);
2001
2076
  }
2002
2077
  function deploymentIdFromPath(url) {
2003
2078
  const match = url.pathname.match(/\/api\/deployments\/([^/]+)(?:\/|$)/);
@@ -2027,6 +2102,7 @@ async function handleSimulatedRequest(request, url) {
2027
2102
  );
2028
2103
  const edgeMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/edge(\/.*)?$/);
2029
2104
  const isStorageHost = url.hostname === STORAGE_TEST_HOST;
2105
+ let signedDeploymentId = null;
2030
2106
  if (requiresDeploymentHmac(request, url)) {
2031
2107
  const authHeader = request.headers.get("X-Stardeck-Auth");
2032
2108
  if (!authHeader) {
@@ -2038,6 +2114,7 @@ async function handleSimulatedRequest(request, url) {
2038
2114
  if (!authPayload || pathDeploymentId !== null && authPayload.deploymentId !== pathDeploymentId) {
2039
2115
  return failure("Invalid authentication", 401);
2040
2116
  }
2117
+ signedDeploymentId = authPayload.deploymentId;
2041
2118
  }
2042
2119
  if (isStorageHost) {
2043
2120
  return handleStorageRequest(request, url);
@@ -2058,6 +2135,9 @@ async function handleSimulatedRequest(request, url) {
2058
2135
  if (edgeMatch) {
2059
2136
  return handleEdgeRequest(request, edgeMatch[1] ?? "");
2060
2137
  }
2138
+ if (url.pathname === "/api/starlens/extract" && request.method === "POST") {
2139
+ return handleStarlensExtract(request, signedDeploymentId);
2140
+ }
2061
2141
  if (dataStoreMatch) {
2062
2142
  const subPath = dataStoreMatch[1] ?? "";
2063
2143
  const db = requireDb();
@@ -2079,7 +2159,7 @@ async function handleSimulatedRequest(request, url) {
2079
2159
  }
2080
2160
  }
2081
2161
  return failure(
2082
- `[stardeck-testing] No simulator for ${request.method} ${url.pathname}. Supported: data-store query/mutate/schema, email send, identities CRUD, payments store/checkout, storage upload/files, messaging send, edge print/display/bindings, auth verify/refresh, Neon /sql.`,
2162
+ `[stardeck-testing] No simulator for ${request.method} ${url.pathname}. Supported: data-store query/mutate/schema, email send, identities CRUD, payments store/checkout, storage upload/files, messaging send, edge print/display/bindings, starlens extract, auth verify/refresh, Neon /sql.`,
2083
2163
  404
2084
2164
  );
2085
2165
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stardeck-customer-apps/testing",
3
- "version": "0.13.0",
3
+ "version": "0.15.0",
4
4
  "description": "Vitest test harness for Stardeck customer apps — in-process Postgres (PGlite) plus a control-plane simulator so the real Stardeck SDKs run unmodified in tests",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -70,7 +70,8 @@
70
70
  "@stardeck-customer-apps/payments-sdk": ">=0.1.0",
71
71
  "@stardeck-customer-apps/storage-sdk": ">=0.1.0",
72
72
  "next": "^14.0.0 || ^15.0.0 || ^16.0.0",
73
- "vitest": ">=2.0.0"
73
+ "vitest": ">=2.0.0",
74
+ "@stardeck-customer-apps/starlens": ">=0.1.0"
74
75
  },
75
76
  "peerDependenciesMeta": {
76
77
  "@stardeck-customer-apps/integrations-sdk": {
@@ -87,6 +88,9 @@
87
88
  },
88
89
  "next": {
89
90
  "optional": true
91
+ },
92
+ "@stardeck-customer-apps/starlens": {
93
+ "optional": true
90
94
  }
91
95
  },
92
96
  "devDependencies": {
@@ -106,6 +110,8 @@
106
110
  "tsup": "^8.0.0",
107
111
  "typescript": "^5.0.0",
108
112
  "typescript-eslint": "^8.0.0",
109
- "vitest": "^3.2.4"
113
+ "vitest": "^3.2.4",
114
+ "@stardeck-customer-apps/starlens": "*",
115
+ "zod": "^4.3.5"
110
116
  }
111
117
  }