@stardeck-customer-apps/testing 0.1.1 → 0.2.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
@@ -85,6 +85,9 @@ describeWorkflow("checkout", () => {
85
85
  auth flows (`__stardeck:sess`).
86
86
  - `app.inbox` — emails sent through the email-sdk: `.latest(addr?)`,
87
87
  `.to(addr)`, `.all()`, `.count`, `.clear()`.
88
+ - `app.identities` — the platform-identity directory created through
89
+ integrations-sdk `client.identities`: `.get(id)`, `.links(id)`, `.all()`,
90
+ `.count`, `.clear()`.
88
91
  - `app.query(sql, params?)` / `app.db` — direct database access for
89
92
  assertions and seeding.
90
93
  - `callRoute(handler, opts)` — invoke an App Router route handler with a real
@@ -104,6 +107,10 @@ describeWorkflow("checkout", () => {
104
107
  - `getSession()` / `requireAuth()` from project-auth — resolves the
105
108
  `asUser(...)` user (header fast path) or issued session cookies.
106
109
  - `EmailClient.send()` — captured in `app.inbox`, never delivered.
110
+ - `client.identities` from integrations-sdk (create/get/update/list accounts &
111
+ persons, attach channel links) — served offline by the simulated directory;
112
+ `update` replaces the `profile` object (not a merge), like the control plane.
113
+ Inspect via `app.identities`. Merge/archive are dashboard-only — not simulated.
107
114
  - `next/headers` (`headers()`/`cookies()`) inside handlers under `callRoute`.
108
115
 
109
116
  ## Rules
package/dist/index.d.mts CHANGED
@@ -43,6 +43,47 @@ interface TestInbox {
43
43
  clear(): void;
44
44
  get count(): number;
45
45
  }
46
+ /**
47
+ * A platform identity captured by the simulated directory. Mirrors the `Identity`
48
+ * shape returned by @stardeck-customer-apps/integrations-sdk's `client.identities`
49
+ * — keep in sync.
50
+ */
51
+ interface CapturedIdentity {
52
+ id: string;
53
+ type: "person" | "account";
54
+ parentId: string | null;
55
+ displayName: string | null;
56
+ profile: Record<string, unknown>;
57
+ status: "active" | "merged" | "archived";
58
+ mergedIntoId: string | null;
59
+ externalRef: string | null;
60
+ createdAt: string;
61
+ updatedAt: string;
62
+ }
63
+ /** A channel identifier / login attached to a person identity. */
64
+ interface CapturedIdentityLink {
65
+ id: string;
66
+ identityId: string;
67
+ kind: string;
68
+ externalId: string;
69
+ verified: boolean;
70
+ createdAt: string;
71
+ }
72
+ /**
73
+ * Inspect the simulated platform-identity directory — the in-memory stand-in for
74
+ * the control plane that `client.identities` talks to in tests. Identities and
75
+ * links created by app code under test land here; assert on them like `inbox`.
76
+ */
77
+ interface TestDirectory {
78
+ /** All identities, oldest first. */
79
+ all(): CapturedIdentity[];
80
+ /** A single identity by id, or undefined. */
81
+ get(id: string): CapturedIdentity | undefined;
82
+ /** Channel/login links attached to an identity. */
83
+ links(identityId: string): CapturedIdentityLink[];
84
+ clear(): void;
85
+ get count(): number;
86
+ }
46
87
  interface TestAppOptions {
47
88
  /**
48
89
  * Path to the schema.sql snapshot generated by
@@ -70,6 +111,8 @@ interface TestApp {
70
111
  db: PGlite;
71
112
  /** Captured outbound email. */
72
113
  inbox: TestInbox;
114
+ /** Inspect the simulated platform-identity directory (`client.identities`). */
115
+ identities: TestDirectory;
73
116
  /** Convenience for raw SQL: `app.query("SELECT ...", [param])`. */
74
117
  query<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<T[]>;
75
118
  /**
@@ -140,4 +183,4 @@ declare const TEST_ENV_DEFAULTS: {
140
183
  /** Default location of the DDL snapshot written by `generate-types`. */
141
184
  declare const DEFAULT_SCHEMA_PATH = "./src/generated/data-store-schema.sql";
142
185
 
143
- export { CONTROL_PLANE_TEST_URL, type CallRouteOptions, type CapturedEmail, DATA_STORE_TEST_HOST, DEFAULT_SCHEMA_PATH, type SessionTokens, TEST_ENV_DEFAULTS, type TestApp, type TestAppOptions, type TestInbox, type TestUser, WORKFLOW_NAME_PREFIX, callRoute, createTestApp, describeWorkflow, parseWorkflowName };
186
+ export { CONTROL_PLANE_TEST_URL, type CallRouteOptions, type CapturedEmail, type CapturedIdentity, type CapturedIdentityLink, DATA_STORE_TEST_HOST, DEFAULT_SCHEMA_PATH, type SessionTokens, TEST_ENV_DEFAULTS, type TestApp, type TestAppOptions, type TestDirectory, type TestInbox, type TestUser, WORKFLOW_NAME_PREFIX, callRoute, createTestApp, describeWorkflow, parseWorkflowName };
package/dist/index.d.ts CHANGED
@@ -43,6 +43,47 @@ interface TestInbox {
43
43
  clear(): void;
44
44
  get count(): number;
45
45
  }
46
+ /**
47
+ * A platform identity captured by the simulated directory. Mirrors the `Identity`
48
+ * shape returned by @stardeck-customer-apps/integrations-sdk's `client.identities`
49
+ * — keep in sync.
50
+ */
51
+ interface CapturedIdentity {
52
+ id: string;
53
+ type: "person" | "account";
54
+ parentId: string | null;
55
+ displayName: string | null;
56
+ profile: Record<string, unknown>;
57
+ status: "active" | "merged" | "archived";
58
+ mergedIntoId: string | null;
59
+ externalRef: string | null;
60
+ createdAt: string;
61
+ updatedAt: string;
62
+ }
63
+ /** A channel identifier / login attached to a person identity. */
64
+ interface CapturedIdentityLink {
65
+ id: string;
66
+ identityId: string;
67
+ kind: string;
68
+ externalId: string;
69
+ verified: boolean;
70
+ createdAt: string;
71
+ }
72
+ /**
73
+ * Inspect the simulated platform-identity directory — the in-memory stand-in for
74
+ * the control plane that `client.identities` talks to in tests. Identities and
75
+ * links created by app code under test land here; assert on them like `inbox`.
76
+ */
77
+ interface TestDirectory {
78
+ /** All identities, oldest first. */
79
+ all(): CapturedIdentity[];
80
+ /** A single identity by id, or undefined. */
81
+ get(id: string): CapturedIdentity | undefined;
82
+ /** Channel/login links attached to an identity. */
83
+ links(identityId: string): CapturedIdentityLink[];
84
+ clear(): void;
85
+ get count(): number;
86
+ }
46
87
  interface TestAppOptions {
47
88
  /**
48
89
  * Path to the schema.sql snapshot generated by
@@ -70,6 +111,8 @@ interface TestApp {
70
111
  db: PGlite;
71
112
  /** Captured outbound email. */
72
113
  inbox: TestInbox;
114
+ /** Inspect the simulated platform-identity directory (`client.identities`). */
115
+ identities: TestDirectory;
73
116
  /** Convenience for raw SQL: `app.query("SELECT ...", [param])`. */
74
117
  query<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<T[]>;
75
118
  /**
@@ -140,4 +183,4 @@ declare const TEST_ENV_DEFAULTS: {
140
183
  /** Default location of the DDL snapshot written by `generate-types`. */
141
184
  declare const DEFAULT_SCHEMA_PATH = "./src/generated/data-store-schema.sql";
142
185
 
143
- export { CONTROL_PLANE_TEST_URL, type CallRouteOptions, type CapturedEmail, DATA_STORE_TEST_HOST, DEFAULT_SCHEMA_PATH, type SessionTokens, TEST_ENV_DEFAULTS, type TestApp, type TestAppOptions, type TestInbox, type TestUser, WORKFLOW_NAME_PREFIX, callRoute, createTestApp, describeWorkflow, parseWorkflowName };
186
+ export { CONTROL_PLANE_TEST_URL, type CallRouteOptions, type CapturedEmail, type CapturedIdentity, type CapturedIdentityLink, DATA_STORE_TEST_HOST, DEFAULT_SCHEMA_PATH, type SessionTokens, TEST_ENV_DEFAULTS, type TestApp, type TestAppOptions, type TestDirectory, type TestInbox, type TestUser, WORKFLOW_NAME_PREFIX, callRoute, createTestApp, describeWorkflow, parseWorkflowName };
package/dist/index.js CHANGED
@@ -43,7 +43,7 @@ __export(index_exports, {
43
43
  module.exports = __toCommonJS(index_exports);
44
44
 
45
45
  // src/test-app.ts
46
- var import_node_crypto3 = __toESM(require("crypto"));
46
+ var import_node_crypto4 = __toESM(require("crypto"));
47
47
  var import_node_fs = require("fs");
48
48
  var import_node_path = require("path");
49
49
 
@@ -96,6 +96,8 @@ var state = globalSingleton("state", () => ({
96
96
  refreshSessions: /* @__PURE__ */ new Map(),
97
97
  emails: [],
98
98
  emailCounter: 0,
99
+ identities: /* @__PURE__ */ new Map(),
100
+ identityLinks: [],
99
101
  allowNetwork: false
100
102
  }));
101
103
  function requireDb() {
@@ -155,8 +157,8 @@ function verifyDeploymentAuthHeader(secret, header) {
155
157
  return null;
156
158
  }
157
159
  if (payload.type !== "deployment-request") return null;
158
- const now = Math.floor(Date.now() / 1e3);
159
- if (Math.abs(now - payload.timestamp) > TIMESTAMP_TOLERANCE_SECONDS) return null;
160
+ const now2 = Math.floor(Date.now() / 1e3);
161
+ if (Math.abs(now2 - payload.timestamp) > TIMESTAMP_TOLERANCE_SECONDS) return null;
160
162
  return payload;
161
163
  }
162
164
 
@@ -460,6 +462,7 @@ var FIELD_TYPE_TO_PG = {
460
462
  currency: "numeric",
461
463
  rating: "integer",
462
464
  relation: "uuid",
465
+ identity: "uuid",
463
466
  file_ref: "jsonb",
464
467
  file_refs: "jsonb",
465
468
  json: "jsonb"
@@ -637,6 +640,153 @@ function createInbox() {
637
640
  };
638
641
  }
639
642
 
643
+ // src/simulator/identities.ts
644
+ var import_node_crypto3 = __toESM(require("crypto"));
645
+ var LINK_KINDS = /* @__PURE__ */ new Set([
646
+ "line",
647
+ "facebook",
648
+ "instagram",
649
+ "email",
650
+ "phone",
651
+ "project_auth_user",
652
+ "dashboard_user"
653
+ ]);
654
+ function now() {
655
+ return (/* @__PURE__ */ new Date()).toISOString();
656
+ }
657
+ function linksFor(identityId) {
658
+ return state.identityLinks.filter((l) => l.identityId === identityId);
659
+ }
660
+ async function readBody(request) {
661
+ try {
662
+ return await request.json();
663
+ } catch {
664
+ return {};
665
+ }
666
+ }
667
+ function handleList(request) {
668
+ const typeParam = new URL(request.url).searchParams.get("type");
669
+ const type = typeParam === "person" || typeParam === "account" ? typeParam : void 0;
670
+ const identities = [...state.identities.values()];
671
+ return success({ identities: type ? identities.filter((i) => i.type === type) : identities });
672
+ }
673
+ async function handleCreate(request) {
674
+ const body = await readBody(request);
675
+ const type = body.type;
676
+ if (type !== "person" && type !== "account") {
677
+ return failure("type must be 'person' or 'account'");
678
+ }
679
+ let parentId = null;
680
+ if (type === "person" && body.parentId) {
681
+ const parent = state.identities.get(body.parentId);
682
+ if (!parent) return failure("parent identity not found");
683
+ if (parent.type !== "account") {
684
+ return failure("a person's parent must be an account");
685
+ }
686
+ parentId = parent.id;
687
+ }
688
+ const identity = {
689
+ id: import_node_crypto3.default.randomUUID(),
690
+ type,
691
+ parentId,
692
+ displayName: body.displayName ?? null,
693
+ profile: body.profile ?? {},
694
+ status: "active",
695
+ mergedIntoId: null,
696
+ externalRef: body.externalRef ?? null,
697
+ createdAt: now(),
698
+ updatedAt: now()
699
+ };
700
+ state.identities.set(identity.id, identity);
701
+ return success({ identity });
702
+ }
703
+ function handleGet(identityId) {
704
+ const identity = state.identities.get(identityId);
705
+ if (!identity) return failure("identity not found", 404);
706
+ return success({ identity, links: linksFor(identityId) });
707
+ }
708
+ async function handleUpdate(identityId, request) {
709
+ const identity = state.identities.get(identityId);
710
+ if (!identity) return failure("identity not found", 404);
711
+ const body = await readBody(request);
712
+ if (body.displayName !== void 0) {
713
+ identity.displayName = body.displayName;
714
+ }
715
+ if (body.profile !== void 0) {
716
+ identity.profile = body.profile;
717
+ }
718
+ if (body.externalRef !== void 0) {
719
+ identity.externalRef = body.externalRef;
720
+ }
721
+ identity.updatedAt = now();
722
+ return success({ identity });
723
+ }
724
+ async function handleAttachLink(identityId, request) {
725
+ const identity = state.identities.get(identityId);
726
+ if (!identity) return failure("identity not found", 404);
727
+ if (identity.type !== "person") {
728
+ return failure("links attach only to a person identity");
729
+ }
730
+ if (identity.status !== "active") {
731
+ return failure("links attach only to active persons");
732
+ }
733
+ const body = await readBody(request);
734
+ const kind = body.kind;
735
+ const externalId = body.externalId;
736
+ if (typeof kind !== "string" || !LINK_KINDS.has(kind)) {
737
+ return failure(`kind must be one of: ${[...LINK_KINDS].join(", ")}`);
738
+ }
739
+ if (typeof externalId !== "string" || !externalId) {
740
+ return failure("externalId is required");
741
+ }
742
+ const existing = state.identityLinks.find((l) => l.kind === kind && l.externalId === externalId);
743
+ if (existing) {
744
+ if (existing.identityId === identityId) return success({ link: existing });
745
+ return failure("identifier already linked to another identity", 409);
746
+ }
747
+ const link = {
748
+ id: import_node_crypto3.default.randomUUID(),
749
+ identityId,
750
+ kind,
751
+ externalId,
752
+ verified: body.verified === true,
753
+ createdAt: now()
754
+ };
755
+ state.identityLinks.push(link);
756
+ return success({ link });
757
+ }
758
+ async function handleIdentitiesRequest(request, subPath) {
759
+ const method = request.method;
760
+ if (subPath === "" || subPath === "/") {
761
+ if (method === "GET") return handleList(request);
762
+ if (method === "POST") return handleCreate(request);
763
+ }
764
+ const linksMatch = subPath.match(/^\/([^/]+)\/links$/);
765
+ if (linksMatch && method === "POST") {
766
+ return handleAttachLink(linksMatch[1], request);
767
+ }
768
+ const singleMatch = subPath.match(/^\/([^/]+)$/);
769
+ if (singleMatch) {
770
+ if (method === "GET") return handleGet(singleMatch[1]);
771
+ if (method === "PATCH") return handleUpdate(singleMatch[1], request);
772
+ }
773
+ return failure(`No identities simulator for ${method} .../identities${subPath}`, 404);
774
+ }
775
+ function createDirectory() {
776
+ return {
777
+ all: () => [...state.identities.values()],
778
+ get: (id) => state.identities.get(id),
779
+ links: (identityId) => linksFor(identityId),
780
+ clear: () => {
781
+ state.identities.clear();
782
+ state.identityLinks = [];
783
+ },
784
+ get count() {
785
+ return state.identities.size;
786
+ }
787
+ };
788
+ }
789
+
640
790
  // src/simulator/router.ts
641
791
  var fetchHolder = globalSingleton("fetch-holder", () => ({
642
792
  originalFetch: null
@@ -654,7 +804,8 @@ async function handleSimulatedRequest(request, url) {
654
804
  }
655
805
  const dataStoreMatch = url.pathname.match(/^\/api\/data-stores\/[^/]+(\/.*)?$/);
656
806
  const isEmail = url.pathname === "/api/email/send";
657
- if (dataStoreMatch || isEmail) {
807
+ const identitiesMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/identities(\/.*)?$/);
808
+ if (dataStoreMatch || isEmail || identitiesMatch) {
658
809
  const authHeader = request.headers.get("X-Stardeck-Auth");
659
810
  if (!authHeader) {
660
811
  return failure("Missing authentication header", 401);
@@ -667,28 +818,31 @@ async function handleSimulatedRequest(request, url) {
667
818
  if (isEmail && request.method === "POST") {
668
819
  return handleEmailSend(request);
669
820
  }
821
+ if (identitiesMatch) {
822
+ return handleIdentitiesRequest(request, identitiesMatch[1] ?? "");
823
+ }
670
824
  if (dataStoreMatch) {
671
825
  const subPath = dataStoreMatch[1] ?? "";
672
826
  const db = requireDb();
673
- const readBody = async () => await request.json();
827
+ const readBody2 = async () => await request.json();
674
828
  if (subPath === "/query" && request.method === "POST") {
675
- return handleQuery(db, await readBody());
829
+ return handleQuery(db, await readBody2());
676
830
  }
677
831
  if (subPath === "/mutate" && request.method === "POST") {
678
- return handleMutate(db, await readBody());
832
+ return handleMutate(db, await readBody2());
679
833
  }
680
834
  if (subPath === "/schema" && request.method === "GET") {
681
835
  return handleGetSchema(db);
682
836
  }
683
837
  if (subPath === "/schema/tables" && request.method === "POST") {
684
- return handleCreateTable(db, await readBody());
838
+ return handleCreateTable(db, await readBody2());
685
839
  }
686
840
  if (subPath === "/schema/columns" && request.method === "POST") {
687
- return handleAddColumn(db, await readBody());
841
+ return handleAddColumn(db, await readBody2());
688
842
  }
689
843
  }
690
844
  return failure(
691
- `[stardeck-testing] No simulator for ${request.method} ${url.pathname}. Supported: data-store query/mutate/schema, email send, auth verify/refresh, Neon /sql.`,
845
+ `[stardeck-testing] No simulator for ${request.method} ${url.pathname}. Supported: data-store query/mutate/schema, email send, identities CRUD, auth verify/refresh, Neon /sql.`,
692
846
  404
693
847
  );
694
848
  }
@@ -773,9 +927,11 @@ async function createTestApp(options = {}) {
773
927
  throw error;
774
928
  }
775
929
  const inbox = createInbox();
930
+ const directory = createDirectory();
776
931
  const app = {
777
932
  db,
778
933
  inbox,
934
+ identities: directory,
779
935
  async query(sql, params = []) {
780
936
  const result = await db.query(sql, params);
781
937
  return result.rows;
@@ -791,8 +947,8 @@ async function createTestApp(options = {}) {
791
947
  issueSession(user) {
792
948
  const fullUser = buildUser(user);
793
949
  const tokens = {
794
- accessToken: `test-access-${import_node_crypto3.default.randomUUID()}`,
795
- refreshToken: `test-refresh-${import_node_crypto3.default.randomUUID()}`
950
+ accessToken: `test-access-${import_node_crypto4.default.randomUUID()}`,
951
+ refreshToken: `test-refresh-${import_node_crypto4.default.randomUUID()}`
796
952
  };
797
953
  state.sessions.set(tokens.accessToken, fullUser);
798
954
  state.refreshSessions.set(tokens.refreshToken, fullUser);
@@ -806,6 +962,8 @@ async function createTestApp(options = {}) {
806
962
  state.refreshSessions.clear();
807
963
  state.emails = [];
808
964
  state.emailCounter = 0;
965
+ state.identities.clear();
966
+ state.identityLinks = [];
809
967
  },
810
968
  async close() {
811
969
  state.db = null;
@@ -813,6 +971,8 @@ async function createTestApp(options = {}) {
813
971
  state.sessions.clear();
814
972
  state.refreshSessions.clear();
815
973
  state.emails = [];
974
+ state.identities.clear();
975
+ state.identityLinks = [];
816
976
  uninstallFetchRouter();
817
977
  await db.close();
818
978
  }
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/test-app.ts
2
- import crypto3 from "crypto";
2
+ import crypto4 from "crypto";
3
3
  import { readFileSync, existsSync } from "fs";
4
4
  import { resolve } from "path";
5
5
 
@@ -52,6 +52,8 @@ var state = globalSingleton("state", () => ({
52
52
  refreshSessions: /* @__PURE__ */ new Map(),
53
53
  emails: [],
54
54
  emailCounter: 0,
55
+ identities: /* @__PURE__ */ new Map(),
56
+ identityLinks: [],
55
57
  allowNetwork: false
56
58
  }));
57
59
  function requireDb() {
@@ -111,8 +113,8 @@ function verifyDeploymentAuthHeader(secret, header) {
111
113
  return null;
112
114
  }
113
115
  if (payload.type !== "deployment-request") return null;
114
- const now = Math.floor(Date.now() / 1e3);
115
- if (Math.abs(now - payload.timestamp) > TIMESTAMP_TOLERANCE_SECONDS) return null;
116
+ const now2 = Math.floor(Date.now() / 1e3);
117
+ if (Math.abs(now2 - payload.timestamp) > TIMESTAMP_TOLERANCE_SECONDS) return null;
116
118
  return payload;
117
119
  }
118
120
 
@@ -416,6 +418,7 @@ var FIELD_TYPE_TO_PG = {
416
418
  currency: "numeric",
417
419
  rating: "integer",
418
420
  relation: "uuid",
421
+ identity: "uuid",
419
422
  file_ref: "jsonb",
420
423
  file_refs: "jsonb",
421
424
  json: "jsonb"
@@ -593,6 +596,153 @@ function createInbox() {
593
596
  };
594
597
  }
595
598
 
599
+ // src/simulator/identities.ts
600
+ import crypto3 from "crypto";
601
+ var LINK_KINDS = /* @__PURE__ */ new Set([
602
+ "line",
603
+ "facebook",
604
+ "instagram",
605
+ "email",
606
+ "phone",
607
+ "project_auth_user",
608
+ "dashboard_user"
609
+ ]);
610
+ function now() {
611
+ return (/* @__PURE__ */ new Date()).toISOString();
612
+ }
613
+ function linksFor(identityId) {
614
+ return state.identityLinks.filter((l) => l.identityId === identityId);
615
+ }
616
+ async function readBody(request) {
617
+ try {
618
+ return await request.json();
619
+ } catch {
620
+ return {};
621
+ }
622
+ }
623
+ function handleList(request) {
624
+ const typeParam = new URL(request.url).searchParams.get("type");
625
+ const type = typeParam === "person" || typeParam === "account" ? typeParam : void 0;
626
+ const identities = [...state.identities.values()];
627
+ return success({ identities: type ? identities.filter((i) => i.type === type) : identities });
628
+ }
629
+ async function handleCreate(request) {
630
+ const body = await readBody(request);
631
+ const type = body.type;
632
+ if (type !== "person" && type !== "account") {
633
+ return failure("type must be 'person' or 'account'");
634
+ }
635
+ let parentId = null;
636
+ if (type === "person" && body.parentId) {
637
+ const parent = state.identities.get(body.parentId);
638
+ if (!parent) return failure("parent identity not found");
639
+ if (parent.type !== "account") {
640
+ return failure("a person's parent must be an account");
641
+ }
642
+ parentId = parent.id;
643
+ }
644
+ const identity = {
645
+ id: crypto3.randomUUID(),
646
+ type,
647
+ parentId,
648
+ displayName: body.displayName ?? null,
649
+ profile: body.profile ?? {},
650
+ status: "active",
651
+ mergedIntoId: null,
652
+ externalRef: body.externalRef ?? null,
653
+ createdAt: now(),
654
+ updatedAt: now()
655
+ };
656
+ state.identities.set(identity.id, identity);
657
+ return success({ identity });
658
+ }
659
+ function handleGet(identityId) {
660
+ const identity = state.identities.get(identityId);
661
+ if (!identity) return failure("identity not found", 404);
662
+ return success({ identity, links: linksFor(identityId) });
663
+ }
664
+ async function handleUpdate(identityId, request) {
665
+ const identity = state.identities.get(identityId);
666
+ if (!identity) return failure("identity not found", 404);
667
+ const body = await readBody(request);
668
+ if (body.displayName !== void 0) {
669
+ identity.displayName = body.displayName;
670
+ }
671
+ if (body.profile !== void 0) {
672
+ identity.profile = body.profile;
673
+ }
674
+ if (body.externalRef !== void 0) {
675
+ identity.externalRef = body.externalRef;
676
+ }
677
+ identity.updatedAt = now();
678
+ return success({ identity });
679
+ }
680
+ async function handleAttachLink(identityId, request) {
681
+ const identity = state.identities.get(identityId);
682
+ if (!identity) return failure("identity not found", 404);
683
+ if (identity.type !== "person") {
684
+ return failure("links attach only to a person identity");
685
+ }
686
+ if (identity.status !== "active") {
687
+ return failure("links attach only to active persons");
688
+ }
689
+ const body = await readBody(request);
690
+ const kind = body.kind;
691
+ const externalId = body.externalId;
692
+ if (typeof kind !== "string" || !LINK_KINDS.has(kind)) {
693
+ return failure(`kind must be one of: ${[...LINK_KINDS].join(", ")}`);
694
+ }
695
+ if (typeof externalId !== "string" || !externalId) {
696
+ return failure("externalId is required");
697
+ }
698
+ const existing = state.identityLinks.find((l) => l.kind === kind && l.externalId === externalId);
699
+ if (existing) {
700
+ if (existing.identityId === identityId) return success({ link: existing });
701
+ return failure("identifier already linked to another identity", 409);
702
+ }
703
+ const link = {
704
+ id: crypto3.randomUUID(),
705
+ identityId,
706
+ kind,
707
+ externalId,
708
+ verified: body.verified === true,
709
+ createdAt: now()
710
+ };
711
+ state.identityLinks.push(link);
712
+ return success({ link });
713
+ }
714
+ async function handleIdentitiesRequest(request, subPath) {
715
+ const method = request.method;
716
+ if (subPath === "" || subPath === "/") {
717
+ if (method === "GET") return handleList(request);
718
+ if (method === "POST") return handleCreate(request);
719
+ }
720
+ const linksMatch = subPath.match(/^\/([^/]+)\/links$/);
721
+ if (linksMatch && method === "POST") {
722
+ return handleAttachLink(linksMatch[1], request);
723
+ }
724
+ const singleMatch = subPath.match(/^\/([^/]+)$/);
725
+ if (singleMatch) {
726
+ if (method === "GET") return handleGet(singleMatch[1]);
727
+ if (method === "PATCH") return handleUpdate(singleMatch[1], request);
728
+ }
729
+ return failure(`No identities simulator for ${method} .../identities${subPath}`, 404);
730
+ }
731
+ function createDirectory() {
732
+ return {
733
+ all: () => [...state.identities.values()],
734
+ get: (id) => state.identities.get(id),
735
+ links: (identityId) => linksFor(identityId),
736
+ clear: () => {
737
+ state.identities.clear();
738
+ state.identityLinks = [];
739
+ },
740
+ get count() {
741
+ return state.identities.size;
742
+ }
743
+ };
744
+ }
745
+
596
746
  // src/simulator/router.ts
597
747
  var fetchHolder = globalSingleton("fetch-holder", () => ({
598
748
  originalFetch: null
@@ -610,7 +760,8 @@ async function handleSimulatedRequest(request, url) {
610
760
  }
611
761
  const dataStoreMatch = url.pathname.match(/^\/api\/data-stores\/[^/]+(\/.*)?$/);
612
762
  const isEmail = url.pathname === "/api/email/send";
613
- if (dataStoreMatch || isEmail) {
763
+ const identitiesMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/identities(\/.*)?$/);
764
+ if (dataStoreMatch || isEmail || identitiesMatch) {
614
765
  const authHeader = request.headers.get("X-Stardeck-Auth");
615
766
  if (!authHeader) {
616
767
  return failure("Missing authentication header", 401);
@@ -623,28 +774,31 @@ async function handleSimulatedRequest(request, url) {
623
774
  if (isEmail && request.method === "POST") {
624
775
  return handleEmailSend(request);
625
776
  }
777
+ if (identitiesMatch) {
778
+ return handleIdentitiesRequest(request, identitiesMatch[1] ?? "");
779
+ }
626
780
  if (dataStoreMatch) {
627
781
  const subPath = dataStoreMatch[1] ?? "";
628
782
  const db = requireDb();
629
- const readBody = async () => await request.json();
783
+ const readBody2 = async () => await request.json();
630
784
  if (subPath === "/query" && request.method === "POST") {
631
- return handleQuery(db, await readBody());
785
+ return handleQuery(db, await readBody2());
632
786
  }
633
787
  if (subPath === "/mutate" && request.method === "POST") {
634
- return handleMutate(db, await readBody());
788
+ return handleMutate(db, await readBody2());
635
789
  }
636
790
  if (subPath === "/schema" && request.method === "GET") {
637
791
  return handleGetSchema(db);
638
792
  }
639
793
  if (subPath === "/schema/tables" && request.method === "POST") {
640
- return handleCreateTable(db, await readBody());
794
+ return handleCreateTable(db, await readBody2());
641
795
  }
642
796
  if (subPath === "/schema/columns" && request.method === "POST") {
643
- return handleAddColumn(db, await readBody());
797
+ return handleAddColumn(db, await readBody2());
644
798
  }
645
799
  }
646
800
  return failure(
647
- `[stardeck-testing] No simulator for ${request.method} ${url.pathname}. Supported: data-store query/mutate/schema, email send, auth verify/refresh, Neon /sql.`,
801
+ `[stardeck-testing] No simulator for ${request.method} ${url.pathname}. Supported: data-store query/mutate/schema, email send, identities CRUD, auth verify/refresh, Neon /sql.`,
648
802
  404
649
803
  );
650
804
  }
@@ -729,9 +883,11 @@ async function createTestApp(options = {}) {
729
883
  throw error;
730
884
  }
731
885
  const inbox = createInbox();
886
+ const directory = createDirectory();
732
887
  const app = {
733
888
  db,
734
889
  inbox,
890
+ identities: directory,
735
891
  async query(sql, params = []) {
736
892
  const result = await db.query(sql, params);
737
893
  return result.rows;
@@ -747,8 +903,8 @@ async function createTestApp(options = {}) {
747
903
  issueSession(user) {
748
904
  const fullUser = buildUser(user);
749
905
  const tokens = {
750
- accessToken: `test-access-${crypto3.randomUUID()}`,
751
- refreshToken: `test-refresh-${crypto3.randomUUID()}`
906
+ accessToken: `test-access-${crypto4.randomUUID()}`,
907
+ refreshToken: `test-refresh-${crypto4.randomUUID()}`
752
908
  };
753
909
  state.sessions.set(tokens.accessToken, fullUser);
754
910
  state.refreshSessions.set(tokens.refreshToken, fullUser);
@@ -762,6 +918,8 @@ async function createTestApp(options = {}) {
762
918
  state.refreshSessions.clear();
763
919
  state.emails = [];
764
920
  state.emailCounter = 0;
921
+ state.identities.clear();
922
+ state.identityLinks = [];
765
923
  },
766
924
  async close() {
767
925
  state.db = null;
@@ -769,6 +927,8 @@ async function createTestApp(options = {}) {
769
927
  state.sessions.clear();
770
928
  state.refreshSessions.clear();
771
929
  state.emails = [];
930
+ state.identities.clear();
931
+ state.identityLinks = [];
772
932
  uninstallFetchRouter();
773
933
  await db.close();
774
934
  }
@@ -44,6 +44,8 @@ var state = globalSingleton("state", () => ({
44
44
  refreshSessions: /* @__PURE__ */ new Map(),
45
45
  emails: [],
46
46
  emailCounter: 0,
47
+ identities: /* @__PURE__ */ new Map(),
48
+ identityLinks: [],
47
49
  allowNetwork: false
48
50
  }));
49
51
 
@@ -17,6 +17,8 @@ var state = globalSingleton("state", () => ({
17
17
  refreshSessions: /* @__PURE__ */ new Map(),
18
18
  emails: [],
19
19
  emailCounter: 0,
20
+ identities: /* @__PURE__ */ new Map(),
21
+ identityLinks: [],
20
22
  allowNetwork: false
21
23
  }));
22
24
 
package/dist/setup.js CHANGED
@@ -38,6 +38,8 @@ var state = globalSingleton("state", () => ({
38
38
  refreshSessions: /* @__PURE__ */ new Map(),
39
39
  emails: [],
40
40
  emailCounter: 0,
41
+ identities: /* @__PURE__ */ new Map(),
42
+ identityLinks: [],
41
43
  allowNetwork: false
42
44
  }));
43
45
  function requireDb() {
@@ -89,8 +91,8 @@ function verifyDeploymentAuthHeader(secret, header) {
89
91
  return null;
90
92
  }
91
93
  if (payload.type !== "deployment-request") return null;
92
- const now = Math.floor(Date.now() / 1e3);
93
- if (Math.abs(now - payload.timestamp) > TIMESTAMP_TOLERANCE_SECONDS) return null;
94
+ const now2 = Math.floor(Date.now() / 1e3);
95
+ if (Math.abs(now2 - payload.timestamp) > TIMESTAMP_TOLERANCE_SECONDS) return null;
94
96
  return payload;
95
97
  }
96
98
 
@@ -394,6 +396,7 @@ var FIELD_TYPE_TO_PG = {
394
396
  currency: "numeric",
395
397
  rating: "integer",
396
398
  relation: "uuid",
399
+ identity: "uuid",
397
400
  file_ref: "jsonb",
398
401
  file_refs: "jsonb",
399
402
  json: "jsonb"
@@ -569,6 +572,139 @@ async function handleEmailSend(request) {
569
572
  return success({ resendId, fromAddress });
570
573
  }
571
574
 
575
+ // src/simulator/identities.ts
576
+ var import_node_crypto3 = __toESM(require("crypto"));
577
+ var LINK_KINDS = /* @__PURE__ */ new Set([
578
+ "line",
579
+ "facebook",
580
+ "instagram",
581
+ "email",
582
+ "phone",
583
+ "project_auth_user",
584
+ "dashboard_user"
585
+ ]);
586
+ function now() {
587
+ return (/* @__PURE__ */ new Date()).toISOString();
588
+ }
589
+ function linksFor(identityId) {
590
+ return state.identityLinks.filter((l) => l.identityId === identityId);
591
+ }
592
+ async function readBody(request) {
593
+ try {
594
+ return await request.json();
595
+ } catch {
596
+ return {};
597
+ }
598
+ }
599
+ function handleList(request) {
600
+ const typeParam = new URL(request.url).searchParams.get("type");
601
+ const type = typeParam === "person" || typeParam === "account" ? typeParam : void 0;
602
+ const identities = [...state.identities.values()];
603
+ return success({ identities: type ? identities.filter((i) => i.type === type) : identities });
604
+ }
605
+ async function handleCreate(request) {
606
+ const body = await readBody(request);
607
+ const type = body.type;
608
+ if (type !== "person" && type !== "account") {
609
+ return failure("type must be 'person' or 'account'");
610
+ }
611
+ let parentId = null;
612
+ if (type === "person" && body.parentId) {
613
+ const parent = state.identities.get(body.parentId);
614
+ if (!parent) return failure("parent identity not found");
615
+ if (parent.type !== "account") {
616
+ return failure("a person's parent must be an account");
617
+ }
618
+ parentId = parent.id;
619
+ }
620
+ const identity = {
621
+ id: import_node_crypto3.default.randomUUID(),
622
+ type,
623
+ parentId,
624
+ displayName: body.displayName ?? null,
625
+ profile: body.profile ?? {},
626
+ status: "active",
627
+ mergedIntoId: null,
628
+ externalRef: body.externalRef ?? null,
629
+ createdAt: now(),
630
+ updatedAt: now()
631
+ };
632
+ state.identities.set(identity.id, identity);
633
+ return success({ identity });
634
+ }
635
+ function handleGet(identityId) {
636
+ const identity = state.identities.get(identityId);
637
+ if (!identity) return failure("identity not found", 404);
638
+ return success({ identity, links: linksFor(identityId) });
639
+ }
640
+ async function handleUpdate(identityId, request) {
641
+ const identity = state.identities.get(identityId);
642
+ if (!identity) return failure("identity not found", 404);
643
+ const body = await readBody(request);
644
+ if (body.displayName !== void 0) {
645
+ identity.displayName = body.displayName;
646
+ }
647
+ if (body.profile !== void 0) {
648
+ identity.profile = body.profile;
649
+ }
650
+ if (body.externalRef !== void 0) {
651
+ identity.externalRef = body.externalRef;
652
+ }
653
+ identity.updatedAt = now();
654
+ return success({ identity });
655
+ }
656
+ async function handleAttachLink(identityId, request) {
657
+ const identity = state.identities.get(identityId);
658
+ if (!identity) return failure("identity not found", 404);
659
+ if (identity.type !== "person") {
660
+ return failure("links attach only to a person identity");
661
+ }
662
+ if (identity.status !== "active") {
663
+ return failure("links attach only to active persons");
664
+ }
665
+ const body = await readBody(request);
666
+ const kind = body.kind;
667
+ const externalId = body.externalId;
668
+ if (typeof kind !== "string" || !LINK_KINDS.has(kind)) {
669
+ return failure(`kind must be one of: ${[...LINK_KINDS].join(", ")}`);
670
+ }
671
+ if (typeof externalId !== "string" || !externalId) {
672
+ return failure("externalId is required");
673
+ }
674
+ const existing = state.identityLinks.find((l) => l.kind === kind && l.externalId === externalId);
675
+ if (existing) {
676
+ if (existing.identityId === identityId) return success({ link: existing });
677
+ return failure("identifier already linked to another identity", 409);
678
+ }
679
+ const link = {
680
+ id: import_node_crypto3.default.randomUUID(),
681
+ identityId,
682
+ kind,
683
+ externalId,
684
+ verified: body.verified === true,
685
+ createdAt: now()
686
+ };
687
+ state.identityLinks.push(link);
688
+ return success({ link });
689
+ }
690
+ async function handleIdentitiesRequest(request, subPath) {
691
+ const method = request.method;
692
+ if (subPath === "" || subPath === "/") {
693
+ if (method === "GET") return handleList(request);
694
+ if (method === "POST") return handleCreate(request);
695
+ }
696
+ const linksMatch = subPath.match(/^\/([^/]+)\/links$/);
697
+ if (linksMatch && method === "POST") {
698
+ return handleAttachLink(linksMatch[1], request);
699
+ }
700
+ const singleMatch = subPath.match(/^\/([^/]+)$/);
701
+ if (singleMatch) {
702
+ if (method === "GET") return handleGet(singleMatch[1]);
703
+ if (method === "PATCH") return handleUpdate(singleMatch[1], request);
704
+ }
705
+ return failure(`No identities simulator for ${method} .../identities${subPath}`, 404);
706
+ }
707
+
572
708
  // src/simulator/router.ts
573
709
  var fetchHolder = globalSingleton("fetch-holder", () => ({
574
710
  originalFetch: null
@@ -586,7 +722,8 @@ async function handleSimulatedRequest(request, url) {
586
722
  }
587
723
  const dataStoreMatch = url.pathname.match(/^\/api\/data-stores\/[^/]+(\/.*)?$/);
588
724
  const isEmail = url.pathname === "/api/email/send";
589
- if (dataStoreMatch || isEmail) {
725
+ const identitiesMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/identities(\/.*)?$/);
726
+ if (dataStoreMatch || isEmail || identitiesMatch) {
590
727
  const authHeader = request.headers.get("X-Stardeck-Auth");
591
728
  if (!authHeader) {
592
729
  return failure("Missing authentication header", 401);
@@ -599,28 +736,31 @@ async function handleSimulatedRequest(request, url) {
599
736
  if (isEmail && request.method === "POST") {
600
737
  return handleEmailSend(request);
601
738
  }
739
+ if (identitiesMatch) {
740
+ return handleIdentitiesRequest(request, identitiesMatch[1] ?? "");
741
+ }
602
742
  if (dataStoreMatch) {
603
743
  const subPath = dataStoreMatch[1] ?? "";
604
744
  const db = requireDb();
605
- const readBody = async () => await request.json();
745
+ const readBody2 = async () => await request.json();
606
746
  if (subPath === "/query" && request.method === "POST") {
607
- return handleQuery(db, await readBody());
747
+ return handleQuery(db, await readBody2());
608
748
  }
609
749
  if (subPath === "/mutate" && request.method === "POST") {
610
- return handleMutate(db, await readBody());
750
+ return handleMutate(db, await readBody2());
611
751
  }
612
752
  if (subPath === "/schema" && request.method === "GET") {
613
753
  return handleGetSchema(db);
614
754
  }
615
755
  if (subPath === "/schema/tables" && request.method === "POST") {
616
- return handleCreateTable(db, await readBody());
756
+ return handleCreateTable(db, await readBody2());
617
757
  }
618
758
  if (subPath === "/schema/columns" && request.method === "POST") {
619
- return handleAddColumn(db, await readBody());
759
+ return handleAddColumn(db, await readBody2());
620
760
  }
621
761
  }
622
762
  return failure(
623
- `[stardeck-testing] No simulator for ${request.method} ${url.pathname}. Supported: data-store query/mutate/schema, email send, auth verify/refresh, Neon /sql.`,
763
+ `[stardeck-testing] No simulator for ${request.method} ${url.pathname}. Supported: data-store query/mutate/schema, email send, identities CRUD, auth verify/refresh, Neon /sql.`,
624
764
  404
625
765
  );
626
766
  }
package/dist/setup.mjs CHANGED
@@ -14,6 +14,8 @@ var state = globalSingleton("state", () => ({
14
14
  refreshSessions: /* @__PURE__ */ new Map(),
15
15
  emails: [],
16
16
  emailCounter: 0,
17
+ identities: /* @__PURE__ */ new Map(),
18
+ identityLinks: [],
17
19
  allowNetwork: false
18
20
  }));
19
21
  function requireDb() {
@@ -65,8 +67,8 @@ function verifyDeploymentAuthHeader(secret, header) {
65
67
  return null;
66
68
  }
67
69
  if (payload.type !== "deployment-request") return null;
68
- const now = Math.floor(Date.now() / 1e3);
69
- if (Math.abs(now - payload.timestamp) > TIMESTAMP_TOLERANCE_SECONDS) return null;
70
+ const now2 = Math.floor(Date.now() / 1e3);
71
+ if (Math.abs(now2 - payload.timestamp) > TIMESTAMP_TOLERANCE_SECONDS) return null;
70
72
  return payload;
71
73
  }
72
74
 
@@ -370,6 +372,7 @@ var FIELD_TYPE_TO_PG = {
370
372
  currency: "numeric",
371
373
  rating: "integer",
372
374
  relation: "uuid",
375
+ identity: "uuid",
373
376
  file_ref: "jsonb",
374
377
  file_refs: "jsonb",
375
378
  json: "jsonb"
@@ -545,6 +548,139 @@ async function handleEmailSend(request) {
545
548
  return success({ resendId, fromAddress });
546
549
  }
547
550
 
551
+ // src/simulator/identities.ts
552
+ import crypto3 from "crypto";
553
+ var LINK_KINDS = /* @__PURE__ */ new Set([
554
+ "line",
555
+ "facebook",
556
+ "instagram",
557
+ "email",
558
+ "phone",
559
+ "project_auth_user",
560
+ "dashboard_user"
561
+ ]);
562
+ function now() {
563
+ return (/* @__PURE__ */ new Date()).toISOString();
564
+ }
565
+ function linksFor(identityId) {
566
+ return state.identityLinks.filter((l) => l.identityId === identityId);
567
+ }
568
+ async function readBody(request) {
569
+ try {
570
+ return await request.json();
571
+ } catch {
572
+ return {};
573
+ }
574
+ }
575
+ function handleList(request) {
576
+ const typeParam = new URL(request.url).searchParams.get("type");
577
+ const type = typeParam === "person" || typeParam === "account" ? typeParam : void 0;
578
+ const identities = [...state.identities.values()];
579
+ return success({ identities: type ? identities.filter((i) => i.type === type) : identities });
580
+ }
581
+ async function handleCreate(request) {
582
+ const body = await readBody(request);
583
+ const type = body.type;
584
+ if (type !== "person" && type !== "account") {
585
+ return failure("type must be 'person' or 'account'");
586
+ }
587
+ let parentId = null;
588
+ if (type === "person" && body.parentId) {
589
+ const parent = state.identities.get(body.parentId);
590
+ if (!parent) return failure("parent identity not found");
591
+ if (parent.type !== "account") {
592
+ return failure("a person's parent must be an account");
593
+ }
594
+ parentId = parent.id;
595
+ }
596
+ const identity = {
597
+ id: crypto3.randomUUID(),
598
+ type,
599
+ parentId,
600
+ displayName: body.displayName ?? null,
601
+ profile: body.profile ?? {},
602
+ status: "active",
603
+ mergedIntoId: null,
604
+ externalRef: body.externalRef ?? null,
605
+ createdAt: now(),
606
+ updatedAt: now()
607
+ };
608
+ state.identities.set(identity.id, identity);
609
+ return success({ identity });
610
+ }
611
+ function handleGet(identityId) {
612
+ const identity = state.identities.get(identityId);
613
+ if (!identity) return failure("identity not found", 404);
614
+ return success({ identity, links: linksFor(identityId) });
615
+ }
616
+ async function handleUpdate(identityId, request) {
617
+ const identity = state.identities.get(identityId);
618
+ if (!identity) return failure("identity not found", 404);
619
+ const body = await readBody(request);
620
+ if (body.displayName !== void 0) {
621
+ identity.displayName = body.displayName;
622
+ }
623
+ if (body.profile !== void 0) {
624
+ identity.profile = body.profile;
625
+ }
626
+ if (body.externalRef !== void 0) {
627
+ identity.externalRef = body.externalRef;
628
+ }
629
+ identity.updatedAt = now();
630
+ return success({ identity });
631
+ }
632
+ async function handleAttachLink(identityId, request) {
633
+ const identity = state.identities.get(identityId);
634
+ if (!identity) return failure("identity not found", 404);
635
+ if (identity.type !== "person") {
636
+ return failure("links attach only to a person identity");
637
+ }
638
+ if (identity.status !== "active") {
639
+ return failure("links attach only to active persons");
640
+ }
641
+ const body = await readBody(request);
642
+ const kind = body.kind;
643
+ const externalId = body.externalId;
644
+ if (typeof kind !== "string" || !LINK_KINDS.has(kind)) {
645
+ return failure(`kind must be one of: ${[...LINK_KINDS].join(", ")}`);
646
+ }
647
+ if (typeof externalId !== "string" || !externalId) {
648
+ return failure("externalId is required");
649
+ }
650
+ const existing = state.identityLinks.find((l) => l.kind === kind && l.externalId === externalId);
651
+ if (existing) {
652
+ if (existing.identityId === identityId) return success({ link: existing });
653
+ return failure("identifier already linked to another identity", 409);
654
+ }
655
+ const link = {
656
+ id: crypto3.randomUUID(),
657
+ identityId,
658
+ kind,
659
+ externalId,
660
+ verified: body.verified === true,
661
+ createdAt: now()
662
+ };
663
+ state.identityLinks.push(link);
664
+ return success({ link });
665
+ }
666
+ async function handleIdentitiesRequest(request, subPath) {
667
+ const method = request.method;
668
+ if (subPath === "" || subPath === "/") {
669
+ if (method === "GET") return handleList(request);
670
+ if (method === "POST") return handleCreate(request);
671
+ }
672
+ const linksMatch = subPath.match(/^\/([^/]+)\/links$/);
673
+ if (linksMatch && method === "POST") {
674
+ return handleAttachLink(linksMatch[1], request);
675
+ }
676
+ const singleMatch = subPath.match(/^\/([^/]+)$/);
677
+ if (singleMatch) {
678
+ if (method === "GET") return handleGet(singleMatch[1]);
679
+ if (method === "PATCH") return handleUpdate(singleMatch[1], request);
680
+ }
681
+ return failure(`No identities simulator for ${method} .../identities${subPath}`, 404);
682
+ }
683
+
548
684
  // src/simulator/router.ts
549
685
  var fetchHolder = globalSingleton("fetch-holder", () => ({
550
686
  originalFetch: null
@@ -562,7 +698,8 @@ async function handleSimulatedRequest(request, url) {
562
698
  }
563
699
  const dataStoreMatch = url.pathname.match(/^\/api\/data-stores\/[^/]+(\/.*)?$/);
564
700
  const isEmail = url.pathname === "/api/email/send";
565
- if (dataStoreMatch || isEmail) {
701
+ const identitiesMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/identities(\/.*)?$/);
702
+ if (dataStoreMatch || isEmail || identitiesMatch) {
566
703
  const authHeader = request.headers.get("X-Stardeck-Auth");
567
704
  if (!authHeader) {
568
705
  return failure("Missing authentication header", 401);
@@ -575,28 +712,31 @@ async function handleSimulatedRequest(request, url) {
575
712
  if (isEmail && request.method === "POST") {
576
713
  return handleEmailSend(request);
577
714
  }
715
+ if (identitiesMatch) {
716
+ return handleIdentitiesRequest(request, identitiesMatch[1] ?? "");
717
+ }
578
718
  if (dataStoreMatch) {
579
719
  const subPath = dataStoreMatch[1] ?? "";
580
720
  const db = requireDb();
581
- const readBody = async () => await request.json();
721
+ const readBody2 = async () => await request.json();
582
722
  if (subPath === "/query" && request.method === "POST") {
583
- return handleQuery(db, await readBody());
723
+ return handleQuery(db, await readBody2());
584
724
  }
585
725
  if (subPath === "/mutate" && request.method === "POST") {
586
- return handleMutate(db, await readBody());
726
+ return handleMutate(db, await readBody2());
587
727
  }
588
728
  if (subPath === "/schema" && request.method === "GET") {
589
729
  return handleGetSchema(db);
590
730
  }
591
731
  if (subPath === "/schema/tables" && request.method === "POST") {
592
- return handleCreateTable(db, await readBody());
732
+ return handleCreateTable(db, await readBody2());
593
733
  }
594
734
  if (subPath === "/schema/columns" && request.method === "POST") {
595
- return handleAddColumn(db, await readBody());
735
+ return handleAddColumn(db, await readBody2());
596
736
  }
597
737
  }
598
738
  return failure(
599
- `[stardeck-testing] No simulator for ${request.method} ${url.pathname}. Supported: data-store query/mutate/schema, email send, auth verify/refresh, Neon /sql.`,
739
+ `[stardeck-testing] No simulator for ${request.method} ${url.pathname}. Supported: data-store query/mutate/schema, email send, identities CRUD, auth verify/refresh, Neon /sql.`,
600
740
  404
601
741
  );
602
742
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stardeck-customer-apps/testing",
3
- "version": "0.1.1",
3
+ "version": "0.2.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",
@@ -78,6 +78,7 @@
78
78
  "@neondatabase/serverless": "^1.0.0",
79
79
  "@stardeck-customer-apps/data-store-sdk": "*",
80
80
  "@stardeck-customer-apps/email-sdk": "*",
81
+ "@stardeck-customer-apps/integrations-sdk": "*",
81
82
  "@stardeck-customer-apps/tsconfig": "*",
82
83
  "@types/node": "^24.10.1",
83
84
  "kysely": "^0.27.0",