@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/SKILL.md +7 -0
- package/dist/index.d.mts +91 -1
- package/dist/index.d.ts +91 -1
- package/dist/index.js +207 -12
- package/dist/index.mjs +209 -12
- package/dist/next/headers-shim.js +2 -0
- package/dist/next/headers-shim.mjs +2 -0
- package/dist/setup.js +149 -9
- package/dist/setup.mjs +149 -9
- package/package.json +8 -2
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/test-app.ts
|
|
2
|
-
import
|
|
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
|
|
115
|
-
if (Math.abs(
|
|
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
|
-
|
|
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
|
|
783
|
+
const readBody2 = async () => await request.json();
|
|
630
784
|
if (subPath === "/query" && request.method === "POST") {
|
|
631
|
-
return handleQuery(db, await
|
|
785
|
+
return handleQuery(db, await readBody2());
|
|
632
786
|
}
|
|
633
787
|
if (subPath === "/mutate" && request.method === "POST") {
|
|
634
|
-
return handleMutate(db, await
|
|
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
|
|
794
|
+
return handleCreateTable(db, await readBody2());
|
|
641
795
|
}
|
|
642
796
|
if (subPath === "/schema/columns" && request.method === "POST") {
|
|
643
|
-
return handleAddColumn(db, await
|
|
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-${
|
|
751
|
-
refreshToken: `test-refresh-${
|
|
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
|
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
|
|
93
|
-
if (Math.abs(
|
|
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
|
-
|
|
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
|
|
745
|
+
const readBody2 = async () => await request.json();
|
|
606
746
|
if (subPath === "/query" && request.method === "POST") {
|
|
607
|
-
return handleQuery(db, await
|
|
747
|
+
return handleQuery(db, await readBody2());
|
|
608
748
|
}
|
|
609
749
|
if (subPath === "/mutate" && request.method === "POST") {
|
|
610
|
-
return handleMutate(db, await
|
|
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
|
|
756
|
+
return handleCreateTable(db, await readBody2());
|
|
617
757
|
}
|
|
618
758
|
if (subPath === "/schema/columns" && request.method === "POST") {
|
|
619
|
-
return handleAddColumn(db, await
|
|
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
|
}
|