@stardeck-customer-apps/testing 0.1.1 → 0.3.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/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
  }
@@ -776,6 +936,42 @@ async function createTestApp(options = {}) {
776
936
  return app;
777
937
  }
778
938
 
939
+ // src/module-app.ts
940
+ import {
941
+ makeSqlPort,
942
+ renderSchemaOpsToSql
943
+ } from "@stardeck-customer-apps/core";
944
+ async function createModuleApp(options) {
945
+ const schemaSql = options.modules.map((m) => renderSchemaOpsToSql(m.schema)).join("\n\n");
946
+ const app = await createTestApp({ schemaSql, allowNetwork: options.allowNetwork });
947
+ const sql = { query: (text, params) => app.db.query(text, params ?? []) };
948
+ const data = makeSqlPort(sql);
949
+ const { createIntegrationsClient } = await import("@stardeck-customer-apps/integrations-sdk");
950
+ const identities = createIntegrationsClient({
951
+ controlPlaneUrl: TEST_ENV_DEFAULTS.CONTROL_PLANE_URL,
952
+ organizationId: TEST_ENV_DEFAULTS.ORGANIZATION_ID,
953
+ projectId: TEST_ENV_DEFAULTS.PROJECT_ID,
954
+ deploymentId: TEST_ENV_DEFAULTS.DEPLOYMENT_ID,
955
+ deploymentSecret: TEST_ENV_DEFAULTS.DEPLOYMENT_SECRET
956
+ }).identities;
957
+ const runSeed = async () => {
958
+ if (options.seed) await options.seed({ data, identities });
959
+ };
960
+ await runSeed();
961
+ return {
962
+ app,
963
+ data,
964
+ identities,
965
+ async reset() {
966
+ await app.reset();
967
+ await runSeed();
968
+ },
969
+ async close() {
970
+ await app.close();
971
+ }
972
+ };
973
+ }
974
+
779
975
  // src/next/headers-shim.ts
780
976
  import { AsyncLocalStorage } from "async_hooks";
781
977
  var requestScopeStorage = globalSingleton(
@@ -872,6 +1068,7 @@ export {
872
1068
  TEST_ENV_DEFAULTS,
873
1069
  WORKFLOW_NAME_PREFIX,
874
1070
  callRoute,
1071
+ createModuleApp,
875
1072
  createTestApp,
876
1073
  describeWorkflow,
877
1074
  parseWorkflowName
@@ -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
  }