@stardeck-customer-apps/testing 0.15.0 → 0.17.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 +71 -5
- package/dist/index.d.mts +70 -0
- package/dist/index.d.ts +70 -0
- package/dist/index.js +392 -48
- package/dist/index.mjs +392 -48
- package/dist/next/headers-shim.js +10 -0
- package/dist/next/headers-shim.mjs +10 -0
- package/dist/setup.js +265 -30
- package/dist/setup.mjs +265 -30
- package/package.json +2 -2
package/dist/setup.js
CHANGED
|
@@ -30,6 +30,23 @@ function globalSingleton(key, create) {
|
|
|
30
30
|
return holder[symbol];
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
// src/constants.ts
|
|
34
|
+
var TEST_DOMAIN_SUFFIX = ".stardeck.test";
|
|
35
|
+
var CONTROL_PLANE_TEST_URL = "https://control-plane.stardeck.test";
|
|
36
|
+
var DATA_STORE_TEST_HOST = "db.stardeck.test";
|
|
37
|
+
var DATA_STORE_TEST_URL = `postgresql://test:test@${DATA_STORE_TEST_HOST}/main`;
|
|
38
|
+
var STORAGE_TEST_URL = "https://storage.stardeck.test";
|
|
39
|
+
var STORAGE_TEST_HOST = "storage.stardeck.test";
|
|
40
|
+
var APP_TEST_ORIGIN = "http://localhost:3333";
|
|
41
|
+
var TEST_ENV_DEFAULTS = {
|
|
42
|
+
CONTROL_PLANE_URL: CONTROL_PLANE_TEST_URL,
|
|
43
|
+
DEPLOYMENT_SECRET: "stardeck-test-deployment-secret",
|
|
44
|
+
ORGANIZATION_ID: "00000000-0000-4000-8000-00000000000a",
|
|
45
|
+
PROJECT_ID: "00000000-0000-4000-8000-00000000000b",
|
|
46
|
+
DEPLOYMENT_ID: "00000000-0000-4000-8000-00000000000c",
|
|
47
|
+
STORAGE_URL: STORAGE_TEST_URL
|
|
48
|
+
};
|
|
49
|
+
|
|
33
50
|
// src/state.ts
|
|
34
51
|
var state = globalSingleton("state", () => ({
|
|
35
52
|
db: null,
|
|
@@ -41,6 +58,11 @@ var state = globalSingleton("state", () => ({
|
|
|
41
58
|
emailCounter: 0,
|
|
42
59
|
identities: /* @__PURE__ */ new Map(),
|
|
43
60
|
identityLinks: [],
|
|
61
|
+
identityClaims: /* @__PURE__ */ new Map(),
|
|
62
|
+
identityStaffAssistEnabled: false,
|
|
63
|
+
identityBearerLinksEnabled: true,
|
|
64
|
+
identitySuppressClaimDelivery: false,
|
|
65
|
+
identityAllowedReturnOrigins: /* @__PURE__ */ new Set([APP_TEST_ORIGIN]),
|
|
44
66
|
checkouts: [],
|
|
45
67
|
checkoutCounter: 0,
|
|
46
68
|
sessionStatuses: /* @__PURE__ */ new Map(),
|
|
@@ -82,22 +104,6 @@ function requireDb() {
|
|
|
82
104
|
return state.db;
|
|
83
105
|
}
|
|
84
106
|
|
|
85
|
-
// src/constants.ts
|
|
86
|
-
var TEST_DOMAIN_SUFFIX = ".stardeck.test";
|
|
87
|
-
var CONTROL_PLANE_TEST_URL = "https://control-plane.stardeck.test";
|
|
88
|
-
var DATA_STORE_TEST_HOST = "db.stardeck.test";
|
|
89
|
-
var DATA_STORE_TEST_URL = `postgresql://test:test@${DATA_STORE_TEST_HOST}/main`;
|
|
90
|
-
var STORAGE_TEST_URL = "https://storage.stardeck.test";
|
|
91
|
-
var STORAGE_TEST_HOST = "storage.stardeck.test";
|
|
92
|
-
var TEST_ENV_DEFAULTS = {
|
|
93
|
-
CONTROL_PLANE_URL: CONTROL_PLANE_TEST_URL,
|
|
94
|
-
DEPLOYMENT_SECRET: "stardeck-test-deployment-secret",
|
|
95
|
-
ORGANIZATION_ID: "00000000-0000-4000-8000-00000000000a",
|
|
96
|
-
PROJECT_ID: "00000000-0000-4000-8000-00000000000b",
|
|
97
|
-
DEPLOYMENT_ID: "00000000-0000-4000-8000-00000000000c",
|
|
98
|
-
STORAGE_URL: STORAGE_TEST_URL
|
|
99
|
-
};
|
|
100
|
-
|
|
101
107
|
// src/simulator/hmac.ts
|
|
102
108
|
var import_node_crypto = __toESM(require("crypto"));
|
|
103
109
|
var TIMESTAMP_TOLERANCE_SECONDS = 300;
|
|
@@ -148,8 +154,8 @@ function json(body, status = 200) {
|
|
|
148
154
|
function success(data) {
|
|
149
155
|
return json({ success: true, data });
|
|
150
156
|
}
|
|
151
|
-
function failure(error, status = 400) {
|
|
152
|
-
return json({ success: false, error }, status);
|
|
157
|
+
function failure(error, status = 400, code) {
|
|
158
|
+
return json({ success: false, error, ...code ? { code } : {} }, status);
|
|
153
159
|
}
|
|
154
160
|
async function readJsonBody(request) {
|
|
155
161
|
try {
|
|
@@ -626,9 +632,17 @@ async function handleEmailSend(request) {
|
|
|
626
632
|
var import_node_crypto3 = __toESM(require("crypto"));
|
|
627
633
|
var ATTACH_LINK_KINDS = /* @__PURE__ */ new Set(["line", "facebook", "instagram", "email", "phone"]);
|
|
628
634
|
var RESOLVE_KINDS = /* @__PURE__ */ new Set(["phone", "email", "line"]);
|
|
635
|
+
var CLAIM_KINDS = /* @__PURE__ */ new Set(["email", "phone"]);
|
|
636
|
+
var CLAIM_FLOWS = /* @__PURE__ */ new Set(["invite", "assist", "transfer", "bearer"]);
|
|
637
|
+
var DUPLICABLE_UNVERIFIED_KINDS = /* @__PURE__ */ new Set(["email", "phone"]);
|
|
638
|
+
var CLAIM_TTL_MS = 48 * 60 * 60 * 1e3;
|
|
639
|
+
var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
640
|
+
var STAFF_ASSISTANCE_NOT_ENABLED = "STAFF_ASSISTANCE_NOT_ENABLED";
|
|
641
|
+
var BEARER_LINKS_NOT_ENABLED = "BEARER_LINKS_NOT_ENABLED";
|
|
629
642
|
var E164_PHONE = /^\+[1-9]\d{1,14}$/;
|
|
630
643
|
var SEARCH_DEFAULT_LIMIT = 50;
|
|
631
644
|
var SEARCH_MAX_LIMIT = 100;
|
|
645
|
+
var TEAM_ACCOUNT_FIELDS_MESSAGE = "This person's name and email come from their team account. Change them in Members settings.";
|
|
632
646
|
function asciiLowercaseTrim(value) {
|
|
633
647
|
const trimmed = value.trim();
|
|
634
648
|
let out = "";
|
|
@@ -708,6 +722,11 @@ function matchingLinks(kind, externalId) {
|
|
|
708
722
|
(link) => link.kind === kind && comparableExternalId(link.kind, link.externalId) === comparable
|
|
709
723
|
);
|
|
710
724
|
}
|
|
725
|
+
function sortedMatchingLinks(kind, externalId) {
|
|
726
|
+
return matchingLinks(kind, externalId).sort(
|
|
727
|
+
(a, b) => Number(b.verified) - Number(a.verified) || a.createdAt.localeCompare(b.createdAt) || state.identityLinks.indexOf(a) - state.identityLinks.indexOf(b)
|
|
728
|
+
);
|
|
729
|
+
}
|
|
711
730
|
function createPerson(displayName) {
|
|
712
731
|
const createdAt = now();
|
|
713
732
|
const identity = {
|
|
@@ -740,11 +759,21 @@ function createUnverifiedLink(identityId, kind, externalId) {
|
|
|
740
759
|
state.identityLinks.push(link);
|
|
741
760
|
return link;
|
|
742
761
|
}
|
|
762
|
+
function isTeamOnly(identityId) {
|
|
763
|
+
const kinds = linksFor(identityId).map((link) => link.kind);
|
|
764
|
+
return kinds.includes("dashboard_user") && !kinds.includes("project_auth_user");
|
|
765
|
+
}
|
|
766
|
+
function isCurrentTeamMember(identityId) {
|
|
767
|
+
return linksFor(identityId).some((link) => link.kind === "dashboard_user" && link.verified);
|
|
768
|
+
}
|
|
769
|
+
function hasRecordedContact(identityId) {
|
|
770
|
+
return linksFor(identityId).some((link) => link.kind === "email" || link.kind === "phone");
|
|
771
|
+
}
|
|
743
772
|
function handleList(request) {
|
|
744
773
|
const typeParam = new URL(request.url).searchParams.get("type");
|
|
745
774
|
const type = typeParam === "person" || typeParam === "account" ? typeParam : void 0;
|
|
746
775
|
const identities = [...state.identities.values()].filter(
|
|
747
|
-
(identity) => identity.status !== "merged"
|
|
776
|
+
(identity) => identity.status !== "merged" && !isTeamOnly(identity.id)
|
|
748
777
|
);
|
|
749
778
|
return success({ identities: type ? identities.filter((i) => i.type === type) : identities });
|
|
750
779
|
}
|
|
@@ -788,11 +817,18 @@ async function handleUpdate(identityId, request) {
|
|
|
788
817
|
const identity = state.identities.get(identityId);
|
|
789
818
|
if (!identity) return failure("identity not found", 404);
|
|
790
819
|
const body = await readJsonBody(request);
|
|
820
|
+
const profile = body.profile;
|
|
821
|
+
if (isCurrentTeamMember(identityId)) {
|
|
822
|
+
const changesName = body.displayName !== void 0 && body.displayName !== identity.displayName;
|
|
823
|
+
const changesEmail = profile !== void 0 && Object.hasOwn(profile, "email") && profile.email !== identity.profile.email;
|
|
824
|
+
if (changesName || changesEmail) return failure(TEAM_ACCOUNT_FIELDS_MESSAGE, 409);
|
|
825
|
+
}
|
|
791
826
|
if (body.displayName !== void 0) {
|
|
792
827
|
identity.displayName = body.displayName;
|
|
793
828
|
}
|
|
794
|
-
if (
|
|
795
|
-
|
|
829
|
+
if (profile !== void 0) {
|
|
830
|
+
const keptEmail = isCurrentTeamMember(identityId) && !Object.hasOwn(profile, "email") && Object.hasOwn(identity.profile, "email");
|
|
831
|
+
identity.profile = keptEmail ? { ...profile, email: identity.profile.email } : profile;
|
|
796
832
|
}
|
|
797
833
|
if (body.externalRef !== void 0) {
|
|
798
834
|
identity.externalRef = body.externalRef;
|
|
@@ -818,9 +854,10 @@ async function handleAttachLink(identityId, request) {
|
|
|
818
854
|
);
|
|
819
855
|
if ("error" in normalized) return failure(normalized.error);
|
|
820
856
|
const { kind, externalId } = normalized;
|
|
821
|
-
const
|
|
822
|
-
|
|
823
|
-
|
|
857
|
+
const matches = matchingLinks(kind, externalId);
|
|
858
|
+
const ownRow = matches.find((link2) => link2.identityId === identityId);
|
|
859
|
+
if (ownRow) return success({ link: ownRow });
|
|
860
|
+
if (!DUPLICABLE_UNVERIFIED_KINDS.has(kind) && matches.length > 0) {
|
|
824
861
|
return failure("identifier already linked to another identity", 409);
|
|
825
862
|
}
|
|
826
863
|
const link = createUnverifiedLink(identityId, kind, externalId);
|
|
@@ -837,7 +874,7 @@ async function handleResolve(request) {
|
|
|
837
874
|
if (body.displayName !== void 0 && typeof body.displayName !== "string") {
|
|
838
875
|
return failure("displayName must be a string");
|
|
839
876
|
}
|
|
840
|
-
const links =
|
|
877
|
+
const links = sortedMatchingLinks(normalized.kind, normalized.externalId);
|
|
841
878
|
if (links.length > 0) {
|
|
842
879
|
const canonicalIds = /* @__PURE__ */ new Set();
|
|
843
880
|
for (const link of links) {
|
|
@@ -848,12 +885,15 @@ async function handleResolve(request) {
|
|
|
848
885
|
return failure(`cannot resolve identity link: ${message}`, 409);
|
|
849
886
|
}
|
|
850
887
|
}
|
|
851
|
-
|
|
888
|
+
const verifiedOwners = new Set(
|
|
889
|
+
links.filter((link) => link.verified).map((link) => link.identityId)
|
|
890
|
+
);
|
|
891
|
+
if (verifiedOwners.size > 1 || !DUPLICABLE_UNVERIFIED_KINDS.has(normalized.kind) && canonicalIds.size !== 1) {
|
|
852
892
|
return failure("identifier is linked to multiple identities", 409);
|
|
853
893
|
}
|
|
854
|
-
const
|
|
855
|
-
const
|
|
856
|
-
if (provenance === "guest" &&
|
|
894
|
+
const [chosen] = links;
|
|
895
|
+
const canonicalId = canonicalIdentity(chosen.identityId).id;
|
|
896
|
+
if (provenance === "guest" && chosen.verified) {
|
|
857
897
|
return success({ identityId: null, reason: "verified_conflict" });
|
|
858
898
|
}
|
|
859
899
|
return success({ identityId: canonicalId, created: false });
|
|
@@ -890,6 +930,36 @@ function identityMatchesQuery(identity, query) {
|
|
|
890
930
|
(link) => link.identityId === identity.id && comparableExternalId(link.kind, link.externalId).toLowerCase().includes(query)
|
|
891
931
|
);
|
|
892
932
|
}
|
|
933
|
+
function parseSearchAttributes(raw) {
|
|
934
|
+
if (raw === null) return {};
|
|
935
|
+
let parsed;
|
|
936
|
+
try {
|
|
937
|
+
parsed = JSON.parse(raw);
|
|
938
|
+
} catch {
|
|
939
|
+
return "attributes: attributes must be a JSON object";
|
|
940
|
+
}
|
|
941
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
942
|
+
return "attributes: attributes must be a JSON object";
|
|
943
|
+
}
|
|
944
|
+
if (Object.hasOwn(parsed, "__proto__")) return "attributes: attribute keys may not be __proto__";
|
|
945
|
+
const entries = Object.entries(parsed);
|
|
946
|
+
if (entries.length > 10) return "attributes: attributes may filter on at most 10 keys";
|
|
947
|
+
for (const [key, value] of entries) {
|
|
948
|
+
if (!["string", "number", "boolean"].includes(typeof value)) {
|
|
949
|
+
return `attributes.${key}: attribute values must be a string, number or boolean`;
|
|
950
|
+
}
|
|
951
|
+
if (key.length < 1 || key.length > 100) {
|
|
952
|
+
return `attributes.${key}: attribute keys must be 1-100 characters`;
|
|
953
|
+
}
|
|
954
|
+
if (typeof value === "string" && value.length > 500) {
|
|
955
|
+
return `attributes.${key}: attribute values must be at most 500 characters`;
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
return parsed;
|
|
959
|
+
}
|
|
960
|
+
function identityMatchesAttributes(identity, attributes) {
|
|
961
|
+
return Object.entries(attributes).every(([key, value]) => identity.profile[key] === value);
|
|
962
|
+
}
|
|
893
963
|
function handleSearch(request) {
|
|
894
964
|
const url = new URL(request.url);
|
|
895
965
|
const query = (url.searchParams.get("query") ?? "").trim().toLowerCase();
|
|
@@ -902,10 +972,14 @@ function handleSearch(request) {
|
|
|
902
972
|
return failure(`limit must be between 1 and ${SEARCH_MAX_LIMIT}`);
|
|
903
973
|
}
|
|
904
974
|
}
|
|
975
|
+
const attributes = parseSearchAttributes(url.searchParams.get("attributes"));
|
|
976
|
+
if (typeof attributes === "string") {
|
|
977
|
+
return json({ success: false, error: "Invalid query parameters", details: attributes }, 400);
|
|
978
|
+
}
|
|
905
979
|
const rawCursor = url.searchParams.get("cursor");
|
|
906
980
|
const cursor = rawCursor ? decodeSearchCursor(rawCursor, query) : null;
|
|
907
981
|
if (rawCursor && !cursor) return failure("cursor is invalid");
|
|
908
|
-
const identities = [...state.identities.values()].filter((identity) => identity.status === "active" && identity.mergedIntoId === null).filter((identity) => identityMatchesQuery(identity, query)).sort(compareIdentityOrder);
|
|
982
|
+
const identities = [...state.identities.values()].filter((identity) => identity.status === "active" && identity.mergedIntoId === null).filter((identity) => !isTeamOnly(identity.id)).filter((identity) => identityMatchesQuery(identity, query)).filter((identity) => identityMatchesAttributes(identity, attributes)).sort(compareIdentityOrder);
|
|
909
983
|
const start = cursor ? identities.findIndex((identity) => compareIdentityOrder(identity, cursor) > 0) : 0;
|
|
910
984
|
const pageStart = start < 0 ? identities.length : start;
|
|
911
985
|
const items = identities.slice(pageStart, pageStart + limit);
|
|
@@ -955,6 +1029,160 @@ async function handleAliases(request) {
|
|
|
955
1029
|
}
|
|
956
1030
|
return success({ aliases });
|
|
957
1031
|
}
|
|
1032
|
+
function claimView(claim) {
|
|
1033
|
+
const state_ = claim.state === "pending" && Date.parse(claim.expiresAt) <= Date.now() ? "expired" : claim.state;
|
|
1034
|
+
return {
|
|
1035
|
+
claimId: claim.claimId,
|
|
1036
|
+
flow: claim.flow,
|
|
1037
|
+
state: state_,
|
|
1038
|
+
refusalReason: claim.refusalReason,
|
|
1039
|
+
completedIdentityId: claim.completedIdentityId,
|
|
1040
|
+
expiresAt: claim.expiresAt
|
|
1041
|
+
};
|
|
1042
|
+
}
|
|
1043
|
+
function validateClaimStaff(flow, staff) {
|
|
1044
|
+
if (flow === "invite") {
|
|
1045
|
+
if (staff !== void 0) return { error: "staff is only accepted for assist or transfer" };
|
|
1046
|
+
return { staffActorRef: null };
|
|
1047
|
+
}
|
|
1048
|
+
if (!staff || typeof staff !== "object") {
|
|
1049
|
+
return { error: `staff is required for a ${flow} claim` };
|
|
1050
|
+
}
|
|
1051
|
+
const { actorRef, grant } = staff;
|
|
1052
|
+
if (typeof actorRef !== "string" || actorRef.length === 0) {
|
|
1053
|
+
return { error: "staff.actorRef is required" };
|
|
1054
|
+
}
|
|
1055
|
+
if (grant !== flow) return { error: `staff.grant must be '${flow}' for a ${flow} claim` };
|
|
1056
|
+
return { staffActorRef: actorRef };
|
|
1057
|
+
}
|
|
1058
|
+
function parseReturnTo(raw) {
|
|
1059
|
+
if (raw === void 0) return { returnTo: null };
|
|
1060
|
+
if (typeof raw !== "string") return { error: "returnTo must be a string" };
|
|
1061
|
+
let parsed;
|
|
1062
|
+
try {
|
|
1063
|
+
parsed = new URL(raw);
|
|
1064
|
+
} catch {
|
|
1065
|
+
return { error: "returnTo must be an absolute URL" };
|
|
1066
|
+
}
|
|
1067
|
+
return { returnTo: state.identityAllowedReturnOrigins.has(parsed.origin) ? raw : null };
|
|
1068
|
+
}
|
|
1069
|
+
function claimableIdentity(identityId) {
|
|
1070
|
+
const identity = state.identities.get(identityId);
|
|
1071
|
+
if (!identity || identity.type !== "person" || identity.status !== "active") return null;
|
|
1072
|
+
return identity;
|
|
1073
|
+
}
|
|
1074
|
+
function mintClaim(params) {
|
|
1075
|
+
const claimId = import_node_crypto3.default.randomUUID();
|
|
1076
|
+
const handle = import_node_crypto3.default.randomBytes(32).toString("base64url");
|
|
1077
|
+
const claim = {
|
|
1078
|
+
claimId,
|
|
1079
|
+
flow: params.flow,
|
|
1080
|
+
identityId: params.identityId,
|
|
1081
|
+
kind: params.kind,
|
|
1082
|
+
externalId: params.externalId,
|
|
1083
|
+
returnTo: params.returnTo,
|
|
1084
|
+
staffActorRef: params.staffActorRef,
|
|
1085
|
+
state: "pending",
|
|
1086
|
+
refusalReason: null,
|
|
1087
|
+
completedIdentityId: null,
|
|
1088
|
+
claimantClerkUserId: null,
|
|
1089
|
+
claimUrl: `${CONTROL_PLANE_TEST_URL}/claim/${handle}`,
|
|
1090
|
+
delivered: params.deliver ? !state.identitySuppressClaimDelivery : null,
|
|
1091
|
+
expiresAt: new Date(Date.now() + CLAIM_TTL_MS).toISOString(),
|
|
1092
|
+
createdAt: now()
|
|
1093
|
+
};
|
|
1094
|
+
state.identityClaims.set(claimId, claim);
|
|
1095
|
+
return success({
|
|
1096
|
+
claimId,
|
|
1097
|
+
claimUrl: claim.claimUrl,
|
|
1098
|
+
expiresAt: claim.expiresAt,
|
|
1099
|
+
// Absent unless asked for: an app delivering its own link must not read a
|
|
1100
|
+
// missing field as "nothing was sent".
|
|
1101
|
+
...params.deliver ? { delivered: claim.delivered } : {}
|
|
1102
|
+
});
|
|
1103
|
+
}
|
|
1104
|
+
function createBearerClaim(body, returnTo) {
|
|
1105
|
+
for (const field of ["kind", "externalId", "deliver", "staff"]) {
|
|
1106
|
+
if (body[field] !== void 0) {
|
|
1107
|
+
return failure(`a bearer claim takes no ${field}: it is raised against no contact`);
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
if (!state.identityBearerLinksEnabled) {
|
|
1111
|
+
return failure(
|
|
1112
|
+
"profile invite links are not enabled for this project",
|
|
1113
|
+
403,
|
|
1114
|
+
BEARER_LINKS_NOT_ENABLED
|
|
1115
|
+
);
|
|
1116
|
+
}
|
|
1117
|
+
const identity = claimableIdentity(body.identityId);
|
|
1118
|
+
if (!identity) return failure("identity not found", 404);
|
|
1119
|
+
if (hasRecordedContact(identity.id)) {
|
|
1120
|
+
return failure("identity has an email or phone on record; send an invite claim instead", 409);
|
|
1121
|
+
}
|
|
1122
|
+
return mintClaim({
|
|
1123
|
+
flow: "bearer",
|
|
1124
|
+
identityId: identity.id,
|
|
1125
|
+
kind: null,
|
|
1126
|
+
externalId: null,
|
|
1127
|
+
returnTo,
|
|
1128
|
+
staffActorRef: null,
|
|
1129
|
+
deliver: false
|
|
1130
|
+
});
|
|
1131
|
+
}
|
|
1132
|
+
async function handleCreateClaim(request) {
|
|
1133
|
+
const body = await readJsonBody(request);
|
|
1134
|
+
const flow = body.flow;
|
|
1135
|
+
if (typeof flow !== "string" || !CLAIM_FLOWS.has(flow)) {
|
|
1136
|
+
return failure("flow must be one of: invite, assist, transfer, bearer");
|
|
1137
|
+
}
|
|
1138
|
+
if (typeof body.identityId !== "string" || !UUID.test(body.identityId)) {
|
|
1139
|
+
return failure("identityId must be a uuid");
|
|
1140
|
+
}
|
|
1141
|
+
const parsedReturnTo = parseReturnTo(body.returnTo);
|
|
1142
|
+
if ("error" in parsedReturnTo) return failure(parsedReturnTo.error);
|
|
1143
|
+
if (flow === "bearer") return createBearerClaim(body, parsedReturnTo.returnTo);
|
|
1144
|
+
const normalized = normalizeLinkInput(
|
|
1145
|
+
body.kind,
|
|
1146
|
+
body.externalId,
|
|
1147
|
+
CLAIM_KINDS,
|
|
1148
|
+
"kind must be one of: email, phone"
|
|
1149
|
+
);
|
|
1150
|
+
if ("error" in normalized) return failure(normalized.error);
|
|
1151
|
+
if (body.deliver !== void 0 && typeof body.deliver !== "boolean") {
|
|
1152
|
+
return failure("deliver must be a boolean");
|
|
1153
|
+
}
|
|
1154
|
+
const staff = validateClaimStaff(flow, body.staff);
|
|
1155
|
+
if ("error" in staff) return failure(staff.error);
|
|
1156
|
+
if (flow !== "invite" && !state.identityStaffAssistEnabled) {
|
|
1157
|
+
return failure(
|
|
1158
|
+
"staff-assisted profile claiming is not enabled for this project",
|
|
1159
|
+
403,
|
|
1160
|
+
STAFF_ASSISTANCE_NOT_ENABLED
|
|
1161
|
+
);
|
|
1162
|
+
}
|
|
1163
|
+
const identity = claimableIdentity(body.identityId);
|
|
1164
|
+
if (!identity) return failure("identity not found", 404);
|
|
1165
|
+
const holdsContact = matchingLinks(normalized.kind, normalized.externalId).some(
|
|
1166
|
+
(link) => link.identityId === identity.id
|
|
1167
|
+
);
|
|
1168
|
+
if (!holdsContact) {
|
|
1169
|
+
return failure("identity does not hold that contact", 409);
|
|
1170
|
+
}
|
|
1171
|
+
return mintClaim({
|
|
1172
|
+
flow,
|
|
1173
|
+
identityId: identity.id,
|
|
1174
|
+
kind: normalized.kind,
|
|
1175
|
+
externalId: normalized.externalId,
|
|
1176
|
+
returnTo: parsedReturnTo.returnTo,
|
|
1177
|
+
staffActorRef: staff.staffActorRef,
|
|
1178
|
+
deliver: body.deliver === true
|
|
1179
|
+
});
|
|
1180
|
+
}
|
|
1181
|
+
function handleGetClaim(claimId) {
|
|
1182
|
+
const claim = state.identityClaims.get(claimId);
|
|
1183
|
+
if (!claim) return failure("claim not found", 404);
|
|
1184
|
+
return success(claimView(claim));
|
|
1185
|
+
}
|
|
958
1186
|
async function handleIdentitiesRequest(request, subPath) {
|
|
959
1187
|
const method = request.method;
|
|
960
1188
|
if (subPath === "" || subPath === "/") {
|
|
@@ -970,6 +1198,13 @@ async function handleIdentitiesRequest(request, subPath) {
|
|
|
970
1198
|
if (subPath === "/search" && method === "GET") {
|
|
971
1199
|
return handleSearch(request);
|
|
972
1200
|
}
|
|
1201
|
+
if (subPath === "/claims" && method === "POST") {
|
|
1202
|
+
return handleCreateClaim(request);
|
|
1203
|
+
}
|
|
1204
|
+
const claimMatch = subPath.match(/^\/claims\/([^/]+)$/);
|
|
1205
|
+
if (claimMatch && method === "GET") {
|
|
1206
|
+
return handleGetClaim(decodeURIComponent(claimMatch[1]));
|
|
1207
|
+
}
|
|
973
1208
|
const linksMatch = subPath.match(/^\/([^/]+)\/links$/);
|
|
974
1209
|
if (linksMatch && method === "POST") {
|
|
975
1210
|
return handleAttachLink(linksMatch[1], request);
|