@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.mjs
CHANGED
|
@@ -6,6 +6,23 @@ function globalSingleton(key, create) {
|
|
|
6
6
|
return holder[symbol];
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
// src/constants.ts
|
|
10
|
+
var TEST_DOMAIN_SUFFIX = ".stardeck.test";
|
|
11
|
+
var CONTROL_PLANE_TEST_URL = "https://control-plane.stardeck.test";
|
|
12
|
+
var DATA_STORE_TEST_HOST = "db.stardeck.test";
|
|
13
|
+
var DATA_STORE_TEST_URL = `postgresql://test:test@${DATA_STORE_TEST_HOST}/main`;
|
|
14
|
+
var STORAGE_TEST_URL = "https://storage.stardeck.test";
|
|
15
|
+
var STORAGE_TEST_HOST = "storage.stardeck.test";
|
|
16
|
+
var APP_TEST_ORIGIN = "http://localhost:3333";
|
|
17
|
+
var TEST_ENV_DEFAULTS = {
|
|
18
|
+
CONTROL_PLANE_URL: CONTROL_PLANE_TEST_URL,
|
|
19
|
+
DEPLOYMENT_SECRET: "stardeck-test-deployment-secret",
|
|
20
|
+
ORGANIZATION_ID: "00000000-0000-4000-8000-00000000000a",
|
|
21
|
+
PROJECT_ID: "00000000-0000-4000-8000-00000000000b",
|
|
22
|
+
DEPLOYMENT_ID: "00000000-0000-4000-8000-00000000000c",
|
|
23
|
+
STORAGE_URL: STORAGE_TEST_URL
|
|
24
|
+
};
|
|
25
|
+
|
|
9
26
|
// src/state.ts
|
|
10
27
|
var state = globalSingleton("state", () => ({
|
|
11
28
|
db: null,
|
|
@@ -17,6 +34,11 @@ var state = globalSingleton("state", () => ({
|
|
|
17
34
|
emailCounter: 0,
|
|
18
35
|
identities: /* @__PURE__ */ new Map(),
|
|
19
36
|
identityLinks: [],
|
|
37
|
+
identityClaims: /* @__PURE__ */ new Map(),
|
|
38
|
+
identityStaffAssistEnabled: false,
|
|
39
|
+
identityBearerLinksEnabled: true,
|
|
40
|
+
identitySuppressClaimDelivery: false,
|
|
41
|
+
identityAllowedReturnOrigins: /* @__PURE__ */ new Set([APP_TEST_ORIGIN]),
|
|
20
42
|
checkouts: [],
|
|
21
43
|
checkoutCounter: 0,
|
|
22
44
|
sessionStatuses: /* @__PURE__ */ new Map(),
|
|
@@ -58,22 +80,6 @@ function requireDb() {
|
|
|
58
80
|
return state.db;
|
|
59
81
|
}
|
|
60
82
|
|
|
61
|
-
// src/constants.ts
|
|
62
|
-
var TEST_DOMAIN_SUFFIX = ".stardeck.test";
|
|
63
|
-
var CONTROL_PLANE_TEST_URL = "https://control-plane.stardeck.test";
|
|
64
|
-
var DATA_STORE_TEST_HOST = "db.stardeck.test";
|
|
65
|
-
var DATA_STORE_TEST_URL = `postgresql://test:test@${DATA_STORE_TEST_HOST}/main`;
|
|
66
|
-
var STORAGE_TEST_URL = "https://storage.stardeck.test";
|
|
67
|
-
var STORAGE_TEST_HOST = "storage.stardeck.test";
|
|
68
|
-
var TEST_ENV_DEFAULTS = {
|
|
69
|
-
CONTROL_PLANE_URL: CONTROL_PLANE_TEST_URL,
|
|
70
|
-
DEPLOYMENT_SECRET: "stardeck-test-deployment-secret",
|
|
71
|
-
ORGANIZATION_ID: "00000000-0000-4000-8000-00000000000a",
|
|
72
|
-
PROJECT_ID: "00000000-0000-4000-8000-00000000000b",
|
|
73
|
-
DEPLOYMENT_ID: "00000000-0000-4000-8000-00000000000c",
|
|
74
|
-
STORAGE_URL: STORAGE_TEST_URL
|
|
75
|
-
};
|
|
76
|
-
|
|
77
83
|
// src/simulator/hmac.ts
|
|
78
84
|
import crypto from "crypto";
|
|
79
85
|
var TIMESTAMP_TOLERANCE_SECONDS = 300;
|
|
@@ -124,8 +130,8 @@ function json(body, status = 200) {
|
|
|
124
130
|
function success(data) {
|
|
125
131
|
return json({ success: true, data });
|
|
126
132
|
}
|
|
127
|
-
function failure(error, status = 400) {
|
|
128
|
-
return json({ success: false, error }, status);
|
|
133
|
+
function failure(error, status = 400, code) {
|
|
134
|
+
return json({ success: false, error, ...code ? { code } : {} }, status);
|
|
129
135
|
}
|
|
130
136
|
async function readJsonBody(request) {
|
|
131
137
|
try {
|
|
@@ -602,9 +608,17 @@ async function handleEmailSend(request) {
|
|
|
602
608
|
import crypto3 from "crypto";
|
|
603
609
|
var ATTACH_LINK_KINDS = /* @__PURE__ */ new Set(["line", "facebook", "instagram", "email", "phone"]);
|
|
604
610
|
var RESOLVE_KINDS = /* @__PURE__ */ new Set(["phone", "email", "line"]);
|
|
611
|
+
var CLAIM_KINDS = /* @__PURE__ */ new Set(["email", "phone"]);
|
|
612
|
+
var CLAIM_FLOWS = /* @__PURE__ */ new Set(["invite", "assist", "transfer", "bearer"]);
|
|
613
|
+
var DUPLICABLE_UNVERIFIED_KINDS = /* @__PURE__ */ new Set(["email", "phone"]);
|
|
614
|
+
var CLAIM_TTL_MS = 48 * 60 * 60 * 1e3;
|
|
615
|
+
var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
616
|
+
var STAFF_ASSISTANCE_NOT_ENABLED = "STAFF_ASSISTANCE_NOT_ENABLED";
|
|
617
|
+
var BEARER_LINKS_NOT_ENABLED = "BEARER_LINKS_NOT_ENABLED";
|
|
605
618
|
var E164_PHONE = /^\+[1-9]\d{1,14}$/;
|
|
606
619
|
var SEARCH_DEFAULT_LIMIT = 50;
|
|
607
620
|
var SEARCH_MAX_LIMIT = 100;
|
|
621
|
+
var TEAM_ACCOUNT_FIELDS_MESSAGE = "This person's name and email come from their team account. Change them in Members settings.";
|
|
608
622
|
function asciiLowercaseTrim(value) {
|
|
609
623
|
const trimmed = value.trim();
|
|
610
624
|
let out = "";
|
|
@@ -684,6 +698,11 @@ function matchingLinks(kind, externalId) {
|
|
|
684
698
|
(link) => link.kind === kind && comparableExternalId(link.kind, link.externalId) === comparable
|
|
685
699
|
);
|
|
686
700
|
}
|
|
701
|
+
function sortedMatchingLinks(kind, externalId) {
|
|
702
|
+
return matchingLinks(kind, externalId).sort(
|
|
703
|
+
(a, b) => Number(b.verified) - Number(a.verified) || a.createdAt.localeCompare(b.createdAt) || state.identityLinks.indexOf(a) - state.identityLinks.indexOf(b)
|
|
704
|
+
);
|
|
705
|
+
}
|
|
687
706
|
function createPerson(displayName) {
|
|
688
707
|
const createdAt = now();
|
|
689
708
|
const identity = {
|
|
@@ -716,11 +735,21 @@ function createUnverifiedLink(identityId, kind, externalId) {
|
|
|
716
735
|
state.identityLinks.push(link);
|
|
717
736
|
return link;
|
|
718
737
|
}
|
|
738
|
+
function isTeamOnly(identityId) {
|
|
739
|
+
const kinds = linksFor(identityId).map((link) => link.kind);
|
|
740
|
+
return kinds.includes("dashboard_user") && !kinds.includes("project_auth_user");
|
|
741
|
+
}
|
|
742
|
+
function isCurrentTeamMember(identityId) {
|
|
743
|
+
return linksFor(identityId).some((link) => link.kind === "dashboard_user" && link.verified);
|
|
744
|
+
}
|
|
745
|
+
function hasRecordedContact(identityId) {
|
|
746
|
+
return linksFor(identityId).some((link) => link.kind === "email" || link.kind === "phone");
|
|
747
|
+
}
|
|
719
748
|
function handleList(request) {
|
|
720
749
|
const typeParam = new URL(request.url).searchParams.get("type");
|
|
721
750
|
const type = typeParam === "person" || typeParam === "account" ? typeParam : void 0;
|
|
722
751
|
const identities = [...state.identities.values()].filter(
|
|
723
|
-
(identity) => identity.status !== "merged"
|
|
752
|
+
(identity) => identity.status !== "merged" && !isTeamOnly(identity.id)
|
|
724
753
|
);
|
|
725
754
|
return success({ identities: type ? identities.filter((i) => i.type === type) : identities });
|
|
726
755
|
}
|
|
@@ -764,11 +793,18 @@ async function handleUpdate(identityId, request) {
|
|
|
764
793
|
const identity = state.identities.get(identityId);
|
|
765
794
|
if (!identity) return failure("identity not found", 404);
|
|
766
795
|
const body = await readJsonBody(request);
|
|
796
|
+
const profile = body.profile;
|
|
797
|
+
if (isCurrentTeamMember(identityId)) {
|
|
798
|
+
const changesName = body.displayName !== void 0 && body.displayName !== identity.displayName;
|
|
799
|
+
const changesEmail = profile !== void 0 && Object.hasOwn(profile, "email") && profile.email !== identity.profile.email;
|
|
800
|
+
if (changesName || changesEmail) return failure(TEAM_ACCOUNT_FIELDS_MESSAGE, 409);
|
|
801
|
+
}
|
|
767
802
|
if (body.displayName !== void 0) {
|
|
768
803
|
identity.displayName = body.displayName;
|
|
769
804
|
}
|
|
770
|
-
if (
|
|
771
|
-
|
|
805
|
+
if (profile !== void 0) {
|
|
806
|
+
const keptEmail = isCurrentTeamMember(identityId) && !Object.hasOwn(profile, "email") && Object.hasOwn(identity.profile, "email");
|
|
807
|
+
identity.profile = keptEmail ? { ...profile, email: identity.profile.email } : profile;
|
|
772
808
|
}
|
|
773
809
|
if (body.externalRef !== void 0) {
|
|
774
810
|
identity.externalRef = body.externalRef;
|
|
@@ -794,9 +830,10 @@ async function handleAttachLink(identityId, request) {
|
|
|
794
830
|
);
|
|
795
831
|
if ("error" in normalized) return failure(normalized.error);
|
|
796
832
|
const { kind, externalId } = normalized;
|
|
797
|
-
const
|
|
798
|
-
|
|
799
|
-
|
|
833
|
+
const matches = matchingLinks(kind, externalId);
|
|
834
|
+
const ownRow = matches.find((link2) => link2.identityId === identityId);
|
|
835
|
+
if (ownRow) return success({ link: ownRow });
|
|
836
|
+
if (!DUPLICABLE_UNVERIFIED_KINDS.has(kind) && matches.length > 0) {
|
|
800
837
|
return failure("identifier already linked to another identity", 409);
|
|
801
838
|
}
|
|
802
839
|
const link = createUnverifiedLink(identityId, kind, externalId);
|
|
@@ -813,7 +850,7 @@ async function handleResolve(request) {
|
|
|
813
850
|
if (body.displayName !== void 0 && typeof body.displayName !== "string") {
|
|
814
851
|
return failure("displayName must be a string");
|
|
815
852
|
}
|
|
816
|
-
const links =
|
|
853
|
+
const links = sortedMatchingLinks(normalized.kind, normalized.externalId);
|
|
817
854
|
if (links.length > 0) {
|
|
818
855
|
const canonicalIds = /* @__PURE__ */ new Set();
|
|
819
856
|
for (const link of links) {
|
|
@@ -824,12 +861,15 @@ async function handleResolve(request) {
|
|
|
824
861
|
return failure(`cannot resolve identity link: ${message}`, 409);
|
|
825
862
|
}
|
|
826
863
|
}
|
|
827
|
-
|
|
864
|
+
const verifiedOwners = new Set(
|
|
865
|
+
links.filter((link) => link.verified).map((link) => link.identityId)
|
|
866
|
+
);
|
|
867
|
+
if (verifiedOwners.size > 1 || !DUPLICABLE_UNVERIFIED_KINDS.has(normalized.kind) && canonicalIds.size !== 1) {
|
|
828
868
|
return failure("identifier is linked to multiple identities", 409);
|
|
829
869
|
}
|
|
830
|
-
const
|
|
831
|
-
const
|
|
832
|
-
if (provenance === "guest" &&
|
|
870
|
+
const [chosen] = links;
|
|
871
|
+
const canonicalId = canonicalIdentity(chosen.identityId).id;
|
|
872
|
+
if (provenance === "guest" && chosen.verified) {
|
|
833
873
|
return success({ identityId: null, reason: "verified_conflict" });
|
|
834
874
|
}
|
|
835
875
|
return success({ identityId: canonicalId, created: false });
|
|
@@ -866,6 +906,36 @@ function identityMatchesQuery(identity, query) {
|
|
|
866
906
|
(link) => link.identityId === identity.id && comparableExternalId(link.kind, link.externalId).toLowerCase().includes(query)
|
|
867
907
|
);
|
|
868
908
|
}
|
|
909
|
+
function parseSearchAttributes(raw) {
|
|
910
|
+
if (raw === null) return {};
|
|
911
|
+
let parsed;
|
|
912
|
+
try {
|
|
913
|
+
parsed = JSON.parse(raw);
|
|
914
|
+
} catch {
|
|
915
|
+
return "attributes: attributes must be a JSON object";
|
|
916
|
+
}
|
|
917
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
918
|
+
return "attributes: attributes must be a JSON object";
|
|
919
|
+
}
|
|
920
|
+
if (Object.hasOwn(parsed, "__proto__")) return "attributes: attribute keys may not be __proto__";
|
|
921
|
+
const entries = Object.entries(parsed);
|
|
922
|
+
if (entries.length > 10) return "attributes: attributes may filter on at most 10 keys";
|
|
923
|
+
for (const [key, value] of entries) {
|
|
924
|
+
if (!["string", "number", "boolean"].includes(typeof value)) {
|
|
925
|
+
return `attributes.${key}: attribute values must be a string, number or boolean`;
|
|
926
|
+
}
|
|
927
|
+
if (key.length < 1 || key.length > 100) {
|
|
928
|
+
return `attributes.${key}: attribute keys must be 1-100 characters`;
|
|
929
|
+
}
|
|
930
|
+
if (typeof value === "string" && value.length > 500) {
|
|
931
|
+
return `attributes.${key}: attribute values must be at most 500 characters`;
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
return parsed;
|
|
935
|
+
}
|
|
936
|
+
function identityMatchesAttributes(identity, attributes) {
|
|
937
|
+
return Object.entries(attributes).every(([key, value]) => identity.profile[key] === value);
|
|
938
|
+
}
|
|
869
939
|
function handleSearch(request) {
|
|
870
940
|
const url = new URL(request.url);
|
|
871
941
|
const query = (url.searchParams.get("query") ?? "").trim().toLowerCase();
|
|
@@ -878,10 +948,14 @@ function handleSearch(request) {
|
|
|
878
948
|
return failure(`limit must be between 1 and ${SEARCH_MAX_LIMIT}`);
|
|
879
949
|
}
|
|
880
950
|
}
|
|
951
|
+
const attributes = parseSearchAttributes(url.searchParams.get("attributes"));
|
|
952
|
+
if (typeof attributes === "string") {
|
|
953
|
+
return json({ success: false, error: "Invalid query parameters", details: attributes }, 400);
|
|
954
|
+
}
|
|
881
955
|
const rawCursor = url.searchParams.get("cursor");
|
|
882
956
|
const cursor = rawCursor ? decodeSearchCursor(rawCursor, query) : null;
|
|
883
957
|
if (rawCursor && !cursor) return failure("cursor is invalid");
|
|
884
|
-
const identities = [...state.identities.values()].filter((identity) => identity.status === "active" && identity.mergedIntoId === null).filter((identity) => identityMatchesQuery(identity, query)).sort(compareIdentityOrder);
|
|
958
|
+
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);
|
|
885
959
|
const start = cursor ? identities.findIndex((identity) => compareIdentityOrder(identity, cursor) > 0) : 0;
|
|
886
960
|
const pageStart = start < 0 ? identities.length : start;
|
|
887
961
|
const items = identities.slice(pageStart, pageStart + limit);
|
|
@@ -931,6 +1005,160 @@ async function handleAliases(request) {
|
|
|
931
1005
|
}
|
|
932
1006
|
return success({ aliases });
|
|
933
1007
|
}
|
|
1008
|
+
function claimView(claim) {
|
|
1009
|
+
const state_ = claim.state === "pending" && Date.parse(claim.expiresAt) <= Date.now() ? "expired" : claim.state;
|
|
1010
|
+
return {
|
|
1011
|
+
claimId: claim.claimId,
|
|
1012
|
+
flow: claim.flow,
|
|
1013
|
+
state: state_,
|
|
1014
|
+
refusalReason: claim.refusalReason,
|
|
1015
|
+
completedIdentityId: claim.completedIdentityId,
|
|
1016
|
+
expiresAt: claim.expiresAt
|
|
1017
|
+
};
|
|
1018
|
+
}
|
|
1019
|
+
function validateClaimStaff(flow, staff) {
|
|
1020
|
+
if (flow === "invite") {
|
|
1021
|
+
if (staff !== void 0) return { error: "staff is only accepted for assist or transfer" };
|
|
1022
|
+
return { staffActorRef: null };
|
|
1023
|
+
}
|
|
1024
|
+
if (!staff || typeof staff !== "object") {
|
|
1025
|
+
return { error: `staff is required for a ${flow} claim` };
|
|
1026
|
+
}
|
|
1027
|
+
const { actorRef, grant } = staff;
|
|
1028
|
+
if (typeof actorRef !== "string" || actorRef.length === 0) {
|
|
1029
|
+
return { error: "staff.actorRef is required" };
|
|
1030
|
+
}
|
|
1031
|
+
if (grant !== flow) return { error: `staff.grant must be '${flow}' for a ${flow} claim` };
|
|
1032
|
+
return { staffActorRef: actorRef };
|
|
1033
|
+
}
|
|
1034
|
+
function parseReturnTo(raw) {
|
|
1035
|
+
if (raw === void 0) return { returnTo: null };
|
|
1036
|
+
if (typeof raw !== "string") return { error: "returnTo must be a string" };
|
|
1037
|
+
let parsed;
|
|
1038
|
+
try {
|
|
1039
|
+
parsed = new URL(raw);
|
|
1040
|
+
} catch {
|
|
1041
|
+
return { error: "returnTo must be an absolute URL" };
|
|
1042
|
+
}
|
|
1043
|
+
return { returnTo: state.identityAllowedReturnOrigins.has(parsed.origin) ? raw : null };
|
|
1044
|
+
}
|
|
1045
|
+
function claimableIdentity(identityId) {
|
|
1046
|
+
const identity = state.identities.get(identityId);
|
|
1047
|
+
if (!identity || identity.type !== "person" || identity.status !== "active") return null;
|
|
1048
|
+
return identity;
|
|
1049
|
+
}
|
|
1050
|
+
function mintClaim(params) {
|
|
1051
|
+
const claimId = crypto3.randomUUID();
|
|
1052
|
+
const handle = crypto3.randomBytes(32).toString("base64url");
|
|
1053
|
+
const claim = {
|
|
1054
|
+
claimId,
|
|
1055
|
+
flow: params.flow,
|
|
1056
|
+
identityId: params.identityId,
|
|
1057
|
+
kind: params.kind,
|
|
1058
|
+
externalId: params.externalId,
|
|
1059
|
+
returnTo: params.returnTo,
|
|
1060
|
+
staffActorRef: params.staffActorRef,
|
|
1061
|
+
state: "pending",
|
|
1062
|
+
refusalReason: null,
|
|
1063
|
+
completedIdentityId: null,
|
|
1064
|
+
claimantClerkUserId: null,
|
|
1065
|
+
claimUrl: `${CONTROL_PLANE_TEST_URL}/claim/${handle}`,
|
|
1066
|
+
delivered: params.deliver ? !state.identitySuppressClaimDelivery : null,
|
|
1067
|
+
expiresAt: new Date(Date.now() + CLAIM_TTL_MS).toISOString(),
|
|
1068
|
+
createdAt: now()
|
|
1069
|
+
};
|
|
1070
|
+
state.identityClaims.set(claimId, claim);
|
|
1071
|
+
return success({
|
|
1072
|
+
claimId,
|
|
1073
|
+
claimUrl: claim.claimUrl,
|
|
1074
|
+
expiresAt: claim.expiresAt,
|
|
1075
|
+
// Absent unless asked for: an app delivering its own link must not read a
|
|
1076
|
+
// missing field as "nothing was sent".
|
|
1077
|
+
...params.deliver ? { delivered: claim.delivered } : {}
|
|
1078
|
+
});
|
|
1079
|
+
}
|
|
1080
|
+
function createBearerClaim(body, returnTo) {
|
|
1081
|
+
for (const field of ["kind", "externalId", "deliver", "staff"]) {
|
|
1082
|
+
if (body[field] !== void 0) {
|
|
1083
|
+
return failure(`a bearer claim takes no ${field}: it is raised against no contact`);
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
if (!state.identityBearerLinksEnabled) {
|
|
1087
|
+
return failure(
|
|
1088
|
+
"profile invite links are not enabled for this project",
|
|
1089
|
+
403,
|
|
1090
|
+
BEARER_LINKS_NOT_ENABLED
|
|
1091
|
+
);
|
|
1092
|
+
}
|
|
1093
|
+
const identity = claimableIdentity(body.identityId);
|
|
1094
|
+
if (!identity) return failure("identity not found", 404);
|
|
1095
|
+
if (hasRecordedContact(identity.id)) {
|
|
1096
|
+
return failure("identity has an email or phone on record; send an invite claim instead", 409);
|
|
1097
|
+
}
|
|
1098
|
+
return mintClaim({
|
|
1099
|
+
flow: "bearer",
|
|
1100
|
+
identityId: identity.id,
|
|
1101
|
+
kind: null,
|
|
1102
|
+
externalId: null,
|
|
1103
|
+
returnTo,
|
|
1104
|
+
staffActorRef: null,
|
|
1105
|
+
deliver: false
|
|
1106
|
+
});
|
|
1107
|
+
}
|
|
1108
|
+
async function handleCreateClaim(request) {
|
|
1109
|
+
const body = await readJsonBody(request);
|
|
1110
|
+
const flow = body.flow;
|
|
1111
|
+
if (typeof flow !== "string" || !CLAIM_FLOWS.has(flow)) {
|
|
1112
|
+
return failure("flow must be one of: invite, assist, transfer, bearer");
|
|
1113
|
+
}
|
|
1114
|
+
if (typeof body.identityId !== "string" || !UUID.test(body.identityId)) {
|
|
1115
|
+
return failure("identityId must be a uuid");
|
|
1116
|
+
}
|
|
1117
|
+
const parsedReturnTo = parseReturnTo(body.returnTo);
|
|
1118
|
+
if ("error" in parsedReturnTo) return failure(parsedReturnTo.error);
|
|
1119
|
+
if (flow === "bearer") return createBearerClaim(body, parsedReturnTo.returnTo);
|
|
1120
|
+
const normalized = normalizeLinkInput(
|
|
1121
|
+
body.kind,
|
|
1122
|
+
body.externalId,
|
|
1123
|
+
CLAIM_KINDS,
|
|
1124
|
+
"kind must be one of: email, phone"
|
|
1125
|
+
);
|
|
1126
|
+
if ("error" in normalized) return failure(normalized.error);
|
|
1127
|
+
if (body.deliver !== void 0 && typeof body.deliver !== "boolean") {
|
|
1128
|
+
return failure("deliver must be a boolean");
|
|
1129
|
+
}
|
|
1130
|
+
const staff = validateClaimStaff(flow, body.staff);
|
|
1131
|
+
if ("error" in staff) return failure(staff.error);
|
|
1132
|
+
if (flow !== "invite" && !state.identityStaffAssistEnabled) {
|
|
1133
|
+
return failure(
|
|
1134
|
+
"staff-assisted profile claiming is not enabled for this project",
|
|
1135
|
+
403,
|
|
1136
|
+
STAFF_ASSISTANCE_NOT_ENABLED
|
|
1137
|
+
);
|
|
1138
|
+
}
|
|
1139
|
+
const identity = claimableIdentity(body.identityId);
|
|
1140
|
+
if (!identity) return failure("identity not found", 404);
|
|
1141
|
+
const holdsContact = matchingLinks(normalized.kind, normalized.externalId).some(
|
|
1142
|
+
(link) => link.identityId === identity.id
|
|
1143
|
+
);
|
|
1144
|
+
if (!holdsContact) {
|
|
1145
|
+
return failure("identity does not hold that contact", 409);
|
|
1146
|
+
}
|
|
1147
|
+
return mintClaim({
|
|
1148
|
+
flow,
|
|
1149
|
+
identityId: identity.id,
|
|
1150
|
+
kind: normalized.kind,
|
|
1151
|
+
externalId: normalized.externalId,
|
|
1152
|
+
returnTo: parsedReturnTo.returnTo,
|
|
1153
|
+
staffActorRef: staff.staffActorRef,
|
|
1154
|
+
deliver: body.deliver === true
|
|
1155
|
+
});
|
|
1156
|
+
}
|
|
1157
|
+
function handleGetClaim(claimId) {
|
|
1158
|
+
const claim = state.identityClaims.get(claimId);
|
|
1159
|
+
if (!claim) return failure("claim not found", 404);
|
|
1160
|
+
return success(claimView(claim));
|
|
1161
|
+
}
|
|
934
1162
|
async function handleIdentitiesRequest(request, subPath) {
|
|
935
1163
|
const method = request.method;
|
|
936
1164
|
if (subPath === "" || subPath === "/") {
|
|
@@ -946,6 +1174,13 @@ async function handleIdentitiesRequest(request, subPath) {
|
|
|
946
1174
|
if (subPath === "/search" && method === "GET") {
|
|
947
1175
|
return handleSearch(request);
|
|
948
1176
|
}
|
|
1177
|
+
if (subPath === "/claims" && method === "POST") {
|
|
1178
|
+
return handleCreateClaim(request);
|
|
1179
|
+
}
|
|
1180
|
+
const claimMatch = subPath.match(/^\/claims\/([^/]+)$/);
|
|
1181
|
+
if (claimMatch && method === "GET") {
|
|
1182
|
+
return handleGetClaim(decodeURIComponent(claimMatch[1]));
|
|
1183
|
+
}
|
|
949
1184
|
const linksMatch = subPath.match(/^\/([^/]+)\/links$/);
|
|
950
1185
|
if (linksMatch && method === "POST") {
|
|
951
1186
|
return handleAttachLink(linksMatch[1], request);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stardeck-customer-apps/testing",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.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",
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
},
|
|
67
67
|
"peerDependencies": {
|
|
68
68
|
"@stardeck-customer-apps/edge-sdk": ">=1.4.0",
|
|
69
|
-
"@stardeck-customer-apps/integrations-sdk": ">=1.
|
|
69
|
+
"@stardeck-customer-apps/integrations-sdk": ">=1.16.0",
|
|
70
70
|
"@stardeck-customer-apps/payments-sdk": ">=0.1.0",
|
|
71
71
|
"@stardeck-customer-apps/storage-sdk": ">=0.1.0",
|
|
72
72
|
"next": "^14.0.0 || ^15.0.0 || ^16.0.0",
|