@stardeck-customer-apps/testing 0.14.0 → 0.16.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 +48 -4
- package/dist/index.d.mts +104 -1
- package/dist/index.d.ts +104 -1
- package/dist/index.js +355 -46
- package/dist/index.mjs +355 -46
- package/dist/next/headers-shim.js +12 -0
- package/dist/next/headers-shim.mjs +12 -0
- package/dist/setup.js +245 -28
- package/dist/setup.mjs +245 -28
- package/package.json +10 -4
package/dist/index.mjs
CHANGED
|
@@ -57,6 +57,31 @@ function globalSingleton(key, create) {
|
|
|
57
57
|
return holder[symbol];
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
// src/constants.ts
|
|
61
|
+
var TEST_DOMAIN_SUFFIX = ".stardeck.test";
|
|
62
|
+
var CONTROL_PLANE_TEST_URL = "https://control-plane.stardeck.test";
|
|
63
|
+
var DATA_STORE_TEST_HOST = "db.stardeck.test";
|
|
64
|
+
var DATA_STORE_TEST_URL = `postgresql://test:test@${DATA_STORE_TEST_HOST}/main`;
|
|
65
|
+
var STORAGE_TEST_URL = "https://storage.stardeck.test";
|
|
66
|
+
var STORAGE_TEST_HOST = "storage.stardeck.test";
|
|
67
|
+
var APP_TEST_ORIGIN = "http://localhost:3333";
|
|
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
|
+
var DEFAULT_TEST_USER = {
|
|
77
|
+
id: "test-user-1",
|
|
78
|
+
email: "test-user@example.com",
|
|
79
|
+
name: "Test User",
|
|
80
|
+
role: "member",
|
|
81
|
+
permissions: []
|
|
82
|
+
};
|
|
83
|
+
var DEFAULT_SCHEMA_PATH = "./src/generated/data-store-schema.sql";
|
|
84
|
+
|
|
60
85
|
// src/state.ts
|
|
61
86
|
var state = globalSingleton("state", () => ({
|
|
62
87
|
db: null,
|
|
@@ -68,6 +93,10 @@ var state = globalSingleton("state", () => ({
|
|
|
68
93
|
emailCounter: 0,
|
|
69
94
|
identities: /* @__PURE__ */ new Map(),
|
|
70
95
|
identityLinks: [],
|
|
96
|
+
identityClaims: /* @__PURE__ */ new Map(),
|
|
97
|
+
identityStaffAssistEnabled: false,
|
|
98
|
+
identitySuppressClaimDelivery: false,
|
|
99
|
+
identityAllowedReturnOrigins: /* @__PURE__ */ new Set([APP_TEST_ORIGIN]),
|
|
71
100
|
checkouts: [],
|
|
72
101
|
checkoutCounter: 0,
|
|
73
102
|
sessionStatuses: /* @__PURE__ */ new Map(),
|
|
@@ -95,6 +124,9 @@ var state = globalSingleton("state", () => ({
|
|
|
95
124
|
edgeBindings: /* @__PURE__ */ new Map(),
|
|
96
125
|
edgeCapabilities: null,
|
|
97
126
|
edgePeripheralCapabilities: /* @__PURE__ */ new Map(),
|
|
127
|
+
starlensCalls: [],
|
|
128
|
+
starlensReplies: [],
|
|
129
|
+
starlensDefaultReply: null,
|
|
98
130
|
allowNetwork: false
|
|
99
131
|
}));
|
|
100
132
|
function requireDb() {
|
|
@@ -106,30 +138,6 @@ function requireDb() {
|
|
|
106
138
|
return state.db;
|
|
107
139
|
}
|
|
108
140
|
|
|
109
|
-
// src/constants.ts
|
|
110
|
-
var TEST_DOMAIN_SUFFIX = ".stardeck.test";
|
|
111
|
-
var CONTROL_PLANE_TEST_URL = "https://control-plane.stardeck.test";
|
|
112
|
-
var DATA_STORE_TEST_HOST = "db.stardeck.test";
|
|
113
|
-
var DATA_STORE_TEST_URL = `postgresql://test:test@${DATA_STORE_TEST_HOST}/main`;
|
|
114
|
-
var STORAGE_TEST_URL = "https://storage.stardeck.test";
|
|
115
|
-
var STORAGE_TEST_HOST = "storage.stardeck.test";
|
|
116
|
-
var TEST_ENV_DEFAULTS = {
|
|
117
|
-
CONTROL_PLANE_URL: CONTROL_PLANE_TEST_URL,
|
|
118
|
-
DEPLOYMENT_SECRET: "stardeck-test-deployment-secret",
|
|
119
|
-
ORGANIZATION_ID: "00000000-0000-4000-8000-00000000000a",
|
|
120
|
-
PROJECT_ID: "00000000-0000-4000-8000-00000000000b",
|
|
121
|
-
DEPLOYMENT_ID: "00000000-0000-4000-8000-00000000000c",
|
|
122
|
-
STORAGE_URL: STORAGE_TEST_URL
|
|
123
|
-
};
|
|
124
|
-
var DEFAULT_TEST_USER = {
|
|
125
|
-
id: "test-user-1",
|
|
126
|
-
email: "test-user@example.com",
|
|
127
|
-
name: "Test User",
|
|
128
|
-
role: "member",
|
|
129
|
-
permissions: []
|
|
130
|
-
};
|
|
131
|
-
var DEFAULT_SCHEMA_PATH = "./src/generated/data-store-schema.sql";
|
|
132
|
-
|
|
133
141
|
// src/simulator/hmac.ts
|
|
134
142
|
import crypto from "crypto";
|
|
135
143
|
var TIMESTAMP_TOLERANCE_SECONDS = 300;
|
|
@@ -180,8 +188,8 @@ function json(body, status = 200) {
|
|
|
180
188
|
function success(data) {
|
|
181
189
|
return json({ success: true, data });
|
|
182
190
|
}
|
|
183
|
-
function failure(error, status = 400) {
|
|
184
|
-
return json({ success: false, error }, status);
|
|
191
|
+
function failure(error, status = 400, code) {
|
|
192
|
+
return json({ success: false, error, ...code ? { code } : {} }, status);
|
|
185
193
|
}
|
|
186
194
|
async function readJsonBody(request) {
|
|
187
195
|
try {
|
|
@@ -669,6 +677,12 @@ var TRUSTED_LINK_KINDS = /* @__PURE__ */ new Set([
|
|
|
669
677
|
"dashboard_user"
|
|
670
678
|
]);
|
|
671
679
|
var RESOLVE_KINDS = /* @__PURE__ */ new Set(["phone", "email", "line"]);
|
|
680
|
+
var CLAIM_KINDS = /* @__PURE__ */ new Set(["email", "phone"]);
|
|
681
|
+
var CLAIM_FLOWS = /* @__PURE__ */ new Set(["invite", "assist", "transfer"]);
|
|
682
|
+
var DUPLICABLE_UNVERIFIED_KINDS = /* @__PURE__ */ new Set(["email", "phone"]);
|
|
683
|
+
var CLAIM_TTL_MS = 48 * 60 * 60 * 1e3;
|
|
684
|
+
var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
685
|
+
var STAFF_ASSISTANCE_NOT_ENABLED = "STAFF_ASSISTANCE_NOT_ENABLED";
|
|
672
686
|
var E164_PHONE = /^\+[1-9]\d{1,14}$/;
|
|
673
687
|
var SEARCH_DEFAULT_LIMIT = 50;
|
|
674
688
|
var SEARCH_MAX_LIMIT = 100;
|
|
@@ -751,6 +765,11 @@ function matchingLinks(kind, externalId) {
|
|
|
751
765
|
(link) => link.kind === kind && comparableExternalId(link.kind, link.externalId) === comparable
|
|
752
766
|
);
|
|
753
767
|
}
|
|
768
|
+
function sortedMatchingLinks(kind, externalId) {
|
|
769
|
+
return matchingLinks(kind, externalId).sort(
|
|
770
|
+
(a, b) => Number(b.verified) - Number(a.verified) || a.createdAt.localeCompare(b.createdAt) || state.identityLinks.indexOf(a) - state.identityLinks.indexOf(b)
|
|
771
|
+
);
|
|
772
|
+
}
|
|
754
773
|
function normalizedLinkKey(link) {
|
|
755
774
|
return `${link.kind}:${comparableExternalId(link.kind, link.externalId)}`;
|
|
756
775
|
}
|
|
@@ -864,9 +883,10 @@ async function handleAttachLink(identityId, request) {
|
|
|
864
883
|
);
|
|
865
884
|
if ("error" in normalized) return failure(normalized.error);
|
|
866
885
|
const { kind, externalId } = normalized;
|
|
867
|
-
const
|
|
868
|
-
|
|
869
|
-
|
|
886
|
+
const matches = matchingLinks(kind, externalId);
|
|
887
|
+
const ownRow = matches.find((link2) => link2.identityId === identityId);
|
|
888
|
+
if (ownRow) return success({ link: ownRow });
|
|
889
|
+
if (!DUPLICABLE_UNVERIFIED_KINDS.has(kind) && matches.length > 0) {
|
|
870
890
|
return failure("identifier already linked to another identity", 409);
|
|
871
891
|
}
|
|
872
892
|
const link = createUnverifiedLink(identityId, kind, externalId);
|
|
@@ -883,7 +903,7 @@ async function handleResolve(request) {
|
|
|
883
903
|
if (body.displayName !== void 0 && typeof body.displayName !== "string") {
|
|
884
904
|
return failure("displayName must be a string");
|
|
885
905
|
}
|
|
886
|
-
const links =
|
|
906
|
+
const links = sortedMatchingLinks(normalized.kind, normalized.externalId);
|
|
887
907
|
if (links.length > 0) {
|
|
888
908
|
const canonicalIds = /* @__PURE__ */ new Set();
|
|
889
909
|
for (const link of links) {
|
|
@@ -894,12 +914,15 @@ async function handleResolve(request) {
|
|
|
894
914
|
return failure(`cannot resolve identity link: ${message}`, 409);
|
|
895
915
|
}
|
|
896
916
|
}
|
|
897
|
-
|
|
917
|
+
const verifiedOwners = new Set(
|
|
918
|
+
links.filter((link) => link.verified).map((link) => link.identityId)
|
|
919
|
+
);
|
|
920
|
+
if (verifiedOwners.size > 1 || !DUPLICABLE_UNVERIFIED_KINDS.has(normalized.kind) && canonicalIds.size !== 1) {
|
|
898
921
|
return failure("identifier is linked to multiple identities", 409);
|
|
899
922
|
}
|
|
900
|
-
const
|
|
901
|
-
const
|
|
902
|
-
if (provenance === "guest" &&
|
|
923
|
+
const [chosen] = links;
|
|
924
|
+
const canonicalId = canonicalIdentity(chosen.identityId).id;
|
|
925
|
+
if (provenance === "guest" && chosen.verified) {
|
|
903
926
|
return success({ identityId: null, reason: "verified_conflict" });
|
|
904
927
|
}
|
|
905
928
|
return success({ identityId: canonicalId, created: false });
|
|
@@ -1001,6 +1024,116 @@ async function handleAliases(request) {
|
|
|
1001
1024
|
}
|
|
1002
1025
|
return success({ aliases });
|
|
1003
1026
|
}
|
|
1027
|
+
function claimView(claim) {
|
|
1028
|
+
const state_ = claim.state === "pending" && Date.parse(claim.expiresAt) <= Date.now() ? "expired" : claim.state;
|
|
1029
|
+
return {
|
|
1030
|
+
claimId: claim.claimId,
|
|
1031
|
+
flow: claim.flow,
|
|
1032
|
+
state: state_,
|
|
1033
|
+
refusalReason: claim.refusalReason,
|
|
1034
|
+
completedIdentityId: claim.completedIdentityId,
|
|
1035
|
+
expiresAt: claim.expiresAt
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
function validateClaimStaff(flow, staff) {
|
|
1039
|
+
if (flow === "invite") {
|
|
1040
|
+
if (staff !== void 0) return { error: "staff is only accepted for assist or transfer" };
|
|
1041
|
+
return { staffActorRef: null };
|
|
1042
|
+
}
|
|
1043
|
+
if (!staff || typeof staff !== "object") {
|
|
1044
|
+
return { error: `staff is required for a ${flow} claim` };
|
|
1045
|
+
}
|
|
1046
|
+
const { actorRef, grant } = staff;
|
|
1047
|
+
if (typeof actorRef !== "string" || actorRef.length === 0) {
|
|
1048
|
+
return { error: "staff.actorRef is required" };
|
|
1049
|
+
}
|
|
1050
|
+
if (grant !== flow) return { error: `staff.grant must be '${flow}' for a ${flow} claim` };
|
|
1051
|
+
return { staffActorRef: actorRef };
|
|
1052
|
+
}
|
|
1053
|
+
async function handleCreateClaim(request) {
|
|
1054
|
+
const body = await readJsonBody(request);
|
|
1055
|
+
const flow = body.flow;
|
|
1056
|
+
if (typeof flow !== "string" || !CLAIM_FLOWS.has(flow)) {
|
|
1057
|
+
return failure("flow must be one of: invite, assist, transfer");
|
|
1058
|
+
}
|
|
1059
|
+
if (typeof body.identityId !== "string" || !UUID.test(body.identityId)) {
|
|
1060
|
+
return failure("identityId must be a uuid");
|
|
1061
|
+
}
|
|
1062
|
+
const normalized = normalizeLinkInput(
|
|
1063
|
+
body.kind,
|
|
1064
|
+
body.externalId,
|
|
1065
|
+
CLAIM_KINDS,
|
|
1066
|
+
"kind must be one of: email, phone"
|
|
1067
|
+
);
|
|
1068
|
+
if ("error" in normalized) return failure(normalized.error);
|
|
1069
|
+
let returnTo = null;
|
|
1070
|
+
if (body.returnTo !== void 0) {
|
|
1071
|
+
if (typeof body.returnTo !== "string") return failure("returnTo must be a string");
|
|
1072
|
+
let parsed;
|
|
1073
|
+
try {
|
|
1074
|
+
parsed = new URL(body.returnTo);
|
|
1075
|
+
} catch {
|
|
1076
|
+
return failure("returnTo must be an absolute URL");
|
|
1077
|
+
}
|
|
1078
|
+
returnTo = state.identityAllowedReturnOrigins.has(parsed.origin) ? body.returnTo : null;
|
|
1079
|
+
}
|
|
1080
|
+
if (body.deliver !== void 0 && typeof body.deliver !== "boolean") {
|
|
1081
|
+
return failure("deliver must be a boolean");
|
|
1082
|
+
}
|
|
1083
|
+
const staff = validateClaimStaff(flow, body.staff);
|
|
1084
|
+
if ("error" in staff) return failure(staff.error);
|
|
1085
|
+
if (flow !== "invite" && !state.identityStaffAssistEnabled) {
|
|
1086
|
+
return failure(
|
|
1087
|
+
"staff-assisted profile claiming is not enabled for this project",
|
|
1088
|
+
403,
|
|
1089
|
+
STAFF_ASSISTANCE_NOT_ENABLED
|
|
1090
|
+
);
|
|
1091
|
+
}
|
|
1092
|
+
const identity = state.identities.get(body.identityId);
|
|
1093
|
+
if (!identity || identity.type !== "person" || identity.status !== "active") {
|
|
1094
|
+
return failure("identity not found", 404);
|
|
1095
|
+
}
|
|
1096
|
+
const holdsContact = matchingLinks(normalized.kind, normalized.externalId).some(
|
|
1097
|
+
(link) => link.identityId === identity.id
|
|
1098
|
+
);
|
|
1099
|
+
if (!holdsContact) {
|
|
1100
|
+
return failure("identity does not hold that contact", 409);
|
|
1101
|
+
}
|
|
1102
|
+
const claimId = crypto3.randomUUID();
|
|
1103
|
+
const deliver = body.deliver === true;
|
|
1104
|
+
const handle = crypto3.randomBytes(32).toString("base64url");
|
|
1105
|
+
const claim = {
|
|
1106
|
+
claimId,
|
|
1107
|
+
flow,
|
|
1108
|
+
identityId: identity.id,
|
|
1109
|
+
kind: normalized.kind,
|
|
1110
|
+
externalId: normalized.externalId,
|
|
1111
|
+
returnTo,
|
|
1112
|
+
staffActorRef: staff.staffActorRef,
|
|
1113
|
+
state: "pending",
|
|
1114
|
+
refusalReason: null,
|
|
1115
|
+
completedIdentityId: null,
|
|
1116
|
+
claimantClerkUserId: null,
|
|
1117
|
+
claimUrl: `${CONTROL_PLANE_TEST_URL}/claim/${handle}`,
|
|
1118
|
+
delivered: deliver ? !state.identitySuppressClaimDelivery : null,
|
|
1119
|
+
expiresAt: new Date(Date.now() + CLAIM_TTL_MS).toISOString(),
|
|
1120
|
+
createdAt: now()
|
|
1121
|
+
};
|
|
1122
|
+
state.identityClaims.set(claimId, claim);
|
|
1123
|
+
return success({
|
|
1124
|
+
claimId,
|
|
1125
|
+
claimUrl: claim.claimUrl,
|
|
1126
|
+
expiresAt: claim.expiresAt,
|
|
1127
|
+
// Absent unless asked for: an app delivering its own link must not read a
|
|
1128
|
+
// missing field as "nothing was sent".
|
|
1129
|
+
...deliver ? { delivered: claim.delivered } : {}
|
|
1130
|
+
});
|
|
1131
|
+
}
|
|
1132
|
+
function handleGetClaim(claimId) {
|
|
1133
|
+
const claim = state.identityClaims.get(claimId);
|
|
1134
|
+
if (!claim) return failure("claim not found", 404);
|
|
1135
|
+
return success(claimView(claim));
|
|
1136
|
+
}
|
|
1004
1137
|
async function handleIdentitiesRequest(request, subPath) {
|
|
1005
1138
|
const method = request.method;
|
|
1006
1139
|
if (subPath === "" || subPath === "/") {
|
|
@@ -1016,6 +1149,13 @@ async function handleIdentitiesRequest(request, subPath) {
|
|
|
1016
1149
|
if (subPath === "/search" && method === "GET") {
|
|
1017
1150
|
return handleSearch(request);
|
|
1018
1151
|
}
|
|
1152
|
+
if (subPath === "/claims" && method === "POST") {
|
|
1153
|
+
return handleCreateClaim(request);
|
|
1154
|
+
}
|
|
1155
|
+
const claimMatch = subPath.match(/^\/claims\/([^/]+)$/);
|
|
1156
|
+
if (claimMatch && method === "GET") {
|
|
1157
|
+
return handleGetClaim(decodeURIComponent(claimMatch[1]));
|
|
1158
|
+
}
|
|
1019
1159
|
const linksMatch = subPath.match(/^\/([^/]+)\/links$/);
|
|
1020
1160
|
if (linksMatch && method === "POST") {
|
|
1021
1161
|
return handleAttachLink(linksMatch[1], request);
|
|
@@ -1040,11 +1180,13 @@ function seedVerifiedLink(identityId, params) {
|
|
|
1040
1180
|
);
|
|
1041
1181
|
if ("error" in normalized) throw new Error(normalized.error);
|
|
1042
1182
|
const { kind, externalId } = normalized;
|
|
1043
|
-
const
|
|
1183
|
+
const matches = matchingLinks(kind, externalId);
|
|
1184
|
+
const blocker = matches.find(
|
|
1185
|
+
(link2) => link2.identityId !== identityId && (link2.verified || !DUPLICABLE_UNVERIFIED_KINDS.has(link2.kind))
|
|
1186
|
+
);
|
|
1187
|
+
if (blocker) throw new Error("identifier already linked to another identity");
|
|
1188
|
+
const existing = matches.find((link2) => link2.identityId === identityId);
|
|
1044
1189
|
if (existing) {
|
|
1045
|
-
if (existing.identityId !== identityId) {
|
|
1046
|
-
throw new Error("identifier already linked to another identity");
|
|
1047
|
-
}
|
|
1048
1190
|
existing.verified = true;
|
|
1049
1191
|
return existing;
|
|
1050
1192
|
}
|
|
@@ -1074,7 +1216,12 @@ function merge(sourceIdentityId, canonicalIdentityId) {
|
|
|
1074
1216
|
);
|
|
1075
1217
|
for (const sourceLink of sourceLinks) {
|
|
1076
1218
|
const key = normalizedLinkKey(sourceLink);
|
|
1077
|
-
|
|
1219
|
+
const conflicting = otherLinks.some(
|
|
1220
|
+
(otherLink) => normalizedLinkKey(otherLink) === key && // Same partial rule as the index: a third identity's UNVERIFIED copy of a
|
|
1221
|
+
// shared email/phone is allowed to coexist and must not block the merge.
|
|
1222
|
+
(!DUPLICABLE_UNVERIFIED_KINDS.has(key.split(":")[0]) || otherLink.verified && sourceLink.verified)
|
|
1223
|
+
);
|
|
1224
|
+
if (conflicting) {
|
|
1078
1225
|
throw new Error("merge would conflict with a link owned by another identity");
|
|
1079
1226
|
}
|
|
1080
1227
|
}
|
|
@@ -1101,6 +1248,45 @@ function merge(sourceIdentityId, canonicalIdentityId) {
|
|
|
1101
1248
|
source.mergedIntoId = canonical.id;
|
|
1102
1249
|
source.updatedAt = now();
|
|
1103
1250
|
}
|
|
1251
|
+
function requireClaim(claimId) {
|
|
1252
|
+
const claim = state.identityClaims.get(claimId);
|
|
1253
|
+
if (!claim) throw new Error("claim not found");
|
|
1254
|
+
return claim;
|
|
1255
|
+
}
|
|
1256
|
+
function completeClaim(claimId, params) {
|
|
1257
|
+
const claim = requireClaim(claimId);
|
|
1258
|
+
if (claim.state !== "pending") return claim;
|
|
1259
|
+
if (Date.parse(claim.expiresAt) <= Date.now()) {
|
|
1260
|
+
claim.state = "expired";
|
|
1261
|
+
return claim;
|
|
1262
|
+
}
|
|
1263
|
+
if (!params.clerkUserId) throw new Error("clerkUserId is required");
|
|
1264
|
+
const rows = matchingLinks(claim.kind, claim.externalId);
|
|
1265
|
+
const target = rows.find((link) => link.identityId === claim.identityId);
|
|
1266
|
+
if (!target) {
|
|
1267
|
+
claim.state = "invalidated";
|
|
1268
|
+
return claim;
|
|
1269
|
+
}
|
|
1270
|
+
const otherVerified = rows.find((link) => link.verified && link.identityId !== claim.identityId);
|
|
1271
|
+
if (otherVerified) {
|
|
1272
|
+
claim.state = "refused";
|
|
1273
|
+
claim.refusalReason = "contact_owned";
|
|
1274
|
+
return claim;
|
|
1275
|
+
}
|
|
1276
|
+
target.verified = true;
|
|
1277
|
+
claim.state = "completed";
|
|
1278
|
+
claim.completedIdentityId = claim.identityId;
|
|
1279
|
+
claim.claimantClerkUserId = params.clerkUserId;
|
|
1280
|
+
return claim;
|
|
1281
|
+
}
|
|
1282
|
+
function refuseClaim(claimId, reason) {
|
|
1283
|
+
const claim = requireClaim(claimId);
|
|
1284
|
+
if (claim.state !== "pending") return claim;
|
|
1285
|
+
if (!reason) throw new Error("a refusal reason is required");
|
|
1286
|
+
claim.state = "refused";
|
|
1287
|
+
claim.refusalReason = reason;
|
|
1288
|
+
return claim;
|
|
1289
|
+
}
|
|
1104
1290
|
function createDirectory() {
|
|
1105
1291
|
return {
|
|
1106
1292
|
all: () => [...state.identities.values()],
|
|
@@ -1108,9 +1294,26 @@ function createDirectory() {
|
|
|
1108
1294
|
links: (identityId) => linksFor(identityId),
|
|
1109
1295
|
seedVerifiedLink,
|
|
1110
1296
|
merge,
|
|
1297
|
+
claims: () => [...state.identityClaims.values()],
|
|
1298
|
+
claim: (claimId) => state.identityClaims.get(claimId),
|
|
1299
|
+
enableStaffAssist: (enabled = true) => {
|
|
1300
|
+
state.identityStaffAssistEnabled = enabled;
|
|
1301
|
+
},
|
|
1302
|
+
suppressClaimDelivery: (enabled = true) => {
|
|
1303
|
+
state.identitySuppressClaimDelivery = enabled;
|
|
1304
|
+
},
|
|
1305
|
+
allowReturnOrigin: (origin) => {
|
|
1306
|
+
state.identityAllowedReturnOrigins.add(new URL(origin).origin);
|
|
1307
|
+
},
|
|
1308
|
+
completeClaim,
|
|
1309
|
+
refuseClaim,
|
|
1111
1310
|
clear: () => {
|
|
1112
1311
|
state.identities.clear();
|
|
1113
1312
|
state.identityLinks = [];
|
|
1313
|
+
state.identityClaims.clear();
|
|
1314
|
+
state.identityStaffAssistEnabled = false;
|
|
1315
|
+
state.identitySuppressClaimDelivery = false;
|
|
1316
|
+
state.identityAllowedReturnOrigins = /* @__PURE__ */ new Set([APP_TEST_ORIGIN]);
|
|
1114
1317
|
},
|
|
1115
1318
|
get count() {
|
|
1116
1319
|
return state.identities.size;
|
|
@@ -1149,7 +1352,7 @@ async function importNextServer() {
|
|
|
1149
1352
|
async function callRoute(handler, options = {}) {
|
|
1150
1353
|
const { NextRequest } = await importNextServer();
|
|
1151
1354
|
const path = options.path ?? "/api/test-route";
|
|
1152
|
-
const url = new URL(
|
|
1355
|
+
const url = new URL(`${APP_TEST_ORIGIN}${path}`);
|
|
1153
1356
|
for (const [key, value] of Object.entries(options.searchParams ?? {})) {
|
|
1154
1357
|
url.searchParams.set(key, value);
|
|
1155
1358
|
}
|
|
@@ -2504,6 +2707,104 @@ function createEdge() {
|
|
|
2504
2707
|
};
|
|
2505
2708
|
}
|
|
2506
2709
|
|
|
2710
|
+
// src/simulator/starlens.ts
|
|
2711
|
+
function starlensFailure(error, status, code) {
|
|
2712
|
+
return json({ success: false, error, code }, status);
|
|
2713
|
+
}
|
|
2714
|
+
function nullFlagged(output, issues) {
|
|
2715
|
+
const copy = JSON.parse(JSON.stringify(output ?? null));
|
|
2716
|
+
for (const { path } of issues) {
|
|
2717
|
+
const keys = path.replace(/^output\./, "").replace(/\[(\d+)\]/g, ".$1").split(".");
|
|
2718
|
+
const last = keys.pop();
|
|
2719
|
+
const parent = keys.reduce(
|
|
2720
|
+
(o, k) => o != null && typeof o === "object" ? o[k] : void 0,
|
|
2721
|
+
copy
|
|
2722
|
+
);
|
|
2723
|
+
if (parent != null && typeof parent === "object" && Object.prototype.hasOwnProperty.call(parent, last)) {
|
|
2724
|
+
parent[last] = null;
|
|
2725
|
+
}
|
|
2726
|
+
}
|
|
2727
|
+
return copy;
|
|
2728
|
+
}
|
|
2729
|
+
async function handleStarlensExtract(request, signedDeploymentId) {
|
|
2730
|
+
const body = await readJsonBody(request);
|
|
2731
|
+
if (signedDeploymentId !== null && body.deploymentId !== signedDeploymentId) {
|
|
2732
|
+
return starlensFailure("Deployment ID mismatch", 401, "AUTHENTICATION_ERROR");
|
|
2733
|
+
}
|
|
2734
|
+
const inputs = Array.isArray(body.inputs) ? body.inputs : [];
|
|
2735
|
+
if (inputs.length === 0) {
|
|
2736
|
+
return starlensFailure(
|
|
2737
|
+
"Validation failed \u2014 inputs: at least one input",
|
|
2738
|
+
400,
|
|
2739
|
+
"VALIDATION_ERROR"
|
|
2740
|
+
);
|
|
2741
|
+
}
|
|
2742
|
+
const schema = body.schema;
|
|
2743
|
+
if (!schema || schema.type !== "object" || typeof schema.properties !== "object") {
|
|
2744
|
+
return starlensFailure(
|
|
2745
|
+
"Validation failed \u2014 schema.properties is required",
|
|
2746
|
+
400,
|
|
2747
|
+
"VALIDATION_ERROR"
|
|
2748
|
+
);
|
|
2749
|
+
}
|
|
2750
|
+
const call = {
|
|
2751
|
+
version: typeof body.version === "string" ? body.version : "latest",
|
|
2752
|
+
inputs: inputs.map((i) => ({
|
|
2753
|
+
name: typeof i.filename === "string" ? i.filename : typeof i.url === "string" ? i.url : "upload",
|
|
2754
|
+
mediaType: typeof i.mediaType === "string" ? i.mediaType : null,
|
|
2755
|
+
source: typeof i.url === "string" ? "url" : "data"
|
|
2756
|
+
})),
|
|
2757
|
+
schema,
|
|
2758
|
+
instructions: typeof body.instructions === "string" ? body.instructions : void 0
|
|
2759
|
+
};
|
|
2760
|
+
state.starlensCalls.push(call);
|
|
2761
|
+
const reply = state.starlensReplies.shift() ?? state.starlensDefaultReply;
|
|
2762
|
+
if (!reply) {
|
|
2763
|
+
return starlensFailure(
|
|
2764
|
+
"[stardeck-testing] No Starlens reply registered. Call app.starlens.mock({ output, issues }) or app.starlens.reject(reason) before the request.",
|
|
2765
|
+
502,
|
|
2766
|
+
"model_failed"
|
|
2767
|
+
);
|
|
2768
|
+
}
|
|
2769
|
+
if ("reject" in reply) {
|
|
2770
|
+
return starlensFailure(reply.reject, 422, "unsupported_document");
|
|
2771
|
+
}
|
|
2772
|
+
const issues = reply.issues ?? [];
|
|
2773
|
+
return success({
|
|
2774
|
+
version: call.version === "latest" ? "v1" : call.version,
|
|
2775
|
+
output: nullFlagged(reply.output, issues),
|
|
2776
|
+
issues,
|
|
2777
|
+
usage: { inputTokens: 0, outputTokens: 0, cost: 0 }
|
|
2778
|
+
});
|
|
2779
|
+
}
|
|
2780
|
+
function createStarlens() {
|
|
2781
|
+
return {
|
|
2782
|
+
mock(reply) {
|
|
2783
|
+
state.starlensReplies.push(reply);
|
|
2784
|
+
},
|
|
2785
|
+
reject(reason) {
|
|
2786
|
+
state.starlensReplies.push({ reject: reason });
|
|
2787
|
+
},
|
|
2788
|
+
mockDefault(reply) {
|
|
2789
|
+
state.starlensDefaultReply = reply;
|
|
2790
|
+
},
|
|
2791
|
+
get calls() {
|
|
2792
|
+
return state.starlensCalls;
|
|
2793
|
+
},
|
|
2794
|
+
latestCall() {
|
|
2795
|
+
return state.starlensCalls[state.starlensCalls.length - 1];
|
|
2796
|
+
},
|
|
2797
|
+
clear() {
|
|
2798
|
+
state.starlensCalls = [];
|
|
2799
|
+
state.starlensReplies = [];
|
|
2800
|
+
state.starlensDefaultReply = null;
|
|
2801
|
+
},
|
|
2802
|
+
get count() {
|
|
2803
|
+
return state.starlensCalls.length;
|
|
2804
|
+
}
|
|
2805
|
+
};
|
|
2806
|
+
}
|
|
2807
|
+
|
|
2507
2808
|
// src/simulator/router.ts
|
|
2508
2809
|
var fetchHolder = globalSingleton("fetch-holder", () => ({
|
|
2509
2810
|
originalFetch: null
|
|
@@ -2525,7 +2826,8 @@ function requiresDeploymentHmac(request, url) {
|
|
|
2525
2826
|
/^\/api\/deployments\/[^/]+\/integrations\/(slack|line|facebook)(\/.*)?$/
|
|
2526
2827
|
);
|
|
2527
2828
|
const edgeMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/edge(\/.*)?$/);
|
|
2528
|
-
|
|
2829
|
+
const isStarlens = url.pathname === "/api/starlens/extract";
|
|
2830
|
+
return !!(isStarlens || dataStoreMatch || isEmail || identitiesMatch || storeMatch || messagingMatch || edgeMatch);
|
|
2529
2831
|
}
|
|
2530
2832
|
function deploymentIdFromPath(url) {
|
|
2531
2833
|
const match = url.pathname.match(/\/api\/deployments\/([^/]+)(?:\/|$)/);
|
|
@@ -2555,6 +2857,7 @@ async function handleSimulatedRequest(request, url) {
|
|
|
2555
2857
|
);
|
|
2556
2858
|
const edgeMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/edge(\/.*)?$/);
|
|
2557
2859
|
const isStorageHost = url.hostname === STORAGE_TEST_HOST;
|
|
2860
|
+
let signedDeploymentId = null;
|
|
2558
2861
|
if (requiresDeploymentHmac(request, url)) {
|
|
2559
2862
|
const authHeader = request.headers.get("X-Stardeck-Auth");
|
|
2560
2863
|
if (!authHeader) {
|
|
@@ -2566,6 +2869,7 @@ async function handleSimulatedRequest(request, url) {
|
|
|
2566
2869
|
if (!authPayload || pathDeploymentId !== null && authPayload.deploymentId !== pathDeploymentId) {
|
|
2567
2870
|
return failure("Invalid authentication", 401);
|
|
2568
2871
|
}
|
|
2872
|
+
signedDeploymentId = authPayload.deploymentId;
|
|
2569
2873
|
}
|
|
2570
2874
|
if (isStorageHost) {
|
|
2571
2875
|
return handleStorageRequest(request, url);
|
|
@@ -2586,6 +2890,9 @@ async function handleSimulatedRequest(request, url) {
|
|
|
2586
2890
|
if (edgeMatch) {
|
|
2587
2891
|
return handleEdgeRequest(request, edgeMatch[1] ?? "");
|
|
2588
2892
|
}
|
|
2893
|
+
if (url.pathname === "/api/starlens/extract" && request.method === "POST") {
|
|
2894
|
+
return handleStarlensExtract(request, signedDeploymentId);
|
|
2895
|
+
}
|
|
2589
2896
|
if (dataStoreMatch) {
|
|
2590
2897
|
const subPath = dataStoreMatch[1] ?? "";
|
|
2591
2898
|
const db = requireDb();
|
|
@@ -2607,7 +2914,7 @@ async function handleSimulatedRequest(request, url) {
|
|
|
2607
2914
|
}
|
|
2608
2915
|
}
|
|
2609
2916
|
return failure(
|
|
2610
|
-
`[stardeck-testing] No simulator for ${request.method} ${url.pathname}. Supported: data-store query/mutate/schema, email send, identities CRUD, payments store/checkout, storage upload/files, messaging send, edge print/display/bindings, auth verify/refresh, Neon /sql.`,
|
|
2917
|
+
`[stardeck-testing] No simulator for ${request.method} ${url.pathname}. Supported: data-store query/mutate/schema, email send, identities CRUD, payments store/checkout, storage upload/files, messaging send, edge print/display/bindings, starlens extract, auth verify/refresh, Neon /sql.`,
|
|
2611
2918
|
404
|
|
2612
2919
|
);
|
|
2613
2920
|
}
|
|
@@ -2775,6 +3082,7 @@ async function createTestApp(options = {}) {
|
|
|
2775
3082
|
const storage = createStorage();
|
|
2776
3083
|
const messages = createMessages();
|
|
2777
3084
|
const edge = createEdge();
|
|
3085
|
+
const starlens = createStarlens();
|
|
2778
3086
|
const app = {
|
|
2779
3087
|
db,
|
|
2780
3088
|
inbox,
|
|
@@ -2783,6 +3091,7 @@ async function createTestApp(options = {}) {
|
|
|
2783
3091
|
storage,
|
|
2784
3092
|
messages,
|
|
2785
3093
|
edge,
|
|
3094
|
+
starlens,
|
|
2786
3095
|
async query(sql, params = []) {
|
|
2787
3096
|
const result = await db.query(sql, params);
|
|
2788
3097
|
return result.rows;
|
|
@@ -2818,12 +3127,12 @@ async function createTestApp(options = {}) {
|
|
|
2818
3127
|
state.refreshSessions.clear();
|
|
2819
3128
|
state.emails = [];
|
|
2820
3129
|
state.emailCounter = 0;
|
|
2821
|
-
|
|
2822
|
-
state.identityLinks = [];
|
|
3130
|
+
directory.clear();
|
|
2823
3131
|
payments.clear();
|
|
2824
3132
|
storage.clear();
|
|
2825
3133
|
messages.clear();
|
|
2826
3134
|
edge.clear();
|
|
3135
|
+
starlens.clear();
|
|
2827
3136
|
},
|
|
2828
3137
|
async close() {
|
|
2829
3138
|
state.db = null;
|
|
@@ -2832,12 +3141,12 @@ async function createTestApp(options = {}) {
|
|
|
2832
3141
|
state.refreshSessions.clear();
|
|
2833
3142
|
state.emails = [];
|
|
2834
3143
|
state.emailCounter = 0;
|
|
2835
|
-
|
|
2836
|
-
state.identityLinks = [];
|
|
3144
|
+
directory.clear();
|
|
2837
3145
|
payments.clear();
|
|
2838
3146
|
storage.clear();
|
|
2839
3147
|
messages.clear();
|
|
2840
3148
|
edge.clear();
|
|
3149
|
+
starlens.clear();
|
|
2841
3150
|
uninstallFetchRouter();
|
|
2842
3151
|
await db.close();
|
|
2843
3152
|
}
|
|
@@ -36,6 +36,11 @@ function globalSingleton(key, create) {
|
|
|
36
36
|
return holder[symbol];
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
// src/constants.ts
|
|
40
|
+
var DATA_STORE_TEST_HOST = "db.stardeck.test";
|
|
41
|
+
var DATA_STORE_TEST_URL = `postgresql://test:test@${DATA_STORE_TEST_HOST}/main`;
|
|
42
|
+
var APP_TEST_ORIGIN = "http://localhost:3333";
|
|
43
|
+
|
|
39
44
|
// src/state.ts
|
|
40
45
|
var state = globalSingleton("state", () => ({
|
|
41
46
|
db: null,
|
|
@@ -47,6 +52,10 @@ var state = globalSingleton("state", () => ({
|
|
|
47
52
|
emailCounter: 0,
|
|
48
53
|
identities: /* @__PURE__ */ new Map(),
|
|
49
54
|
identityLinks: [],
|
|
55
|
+
identityClaims: /* @__PURE__ */ new Map(),
|
|
56
|
+
identityStaffAssistEnabled: false,
|
|
57
|
+
identitySuppressClaimDelivery: false,
|
|
58
|
+
identityAllowedReturnOrigins: /* @__PURE__ */ new Set([APP_TEST_ORIGIN]),
|
|
50
59
|
checkouts: [],
|
|
51
60
|
checkoutCounter: 0,
|
|
52
61
|
sessionStatuses: /* @__PURE__ */ new Map(),
|
|
@@ -74,6 +83,9 @@ var state = globalSingleton("state", () => ({
|
|
|
74
83
|
edgeBindings: /* @__PURE__ */ new Map(),
|
|
75
84
|
edgeCapabilities: null,
|
|
76
85
|
edgePeripheralCapabilities: /* @__PURE__ */ new Map(),
|
|
86
|
+
starlensCalls: [],
|
|
87
|
+
starlensReplies: [],
|
|
88
|
+
starlensDefaultReply: null,
|
|
77
89
|
allowNetwork: false
|
|
78
90
|
}));
|
|
79
91
|
|
|
@@ -9,6 +9,11 @@ function globalSingleton(key, create) {
|
|
|
9
9
|
return holder[symbol];
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
// src/constants.ts
|
|
13
|
+
var DATA_STORE_TEST_HOST = "db.stardeck.test";
|
|
14
|
+
var DATA_STORE_TEST_URL = `postgresql://test:test@${DATA_STORE_TEST_HOST}/main`;
|
|
15
|
+
var APP_TEST_ORIGIN = "http://localhost:3333";
|
|
16
|
+
|
|
12
17
|
// src/state.ts
|
|
13
18
|
var state = globalSingleton("state", () => ({
|
|
14
19
|
db: null,
|
|
@@ -20,6 +25,10 @@ var state = globalSingleton("state", () => ({
|
|
|
20
25
|
emailCounter: 0,
|
|
21
26
|
identities: /* @__PURE__ */ new Map(),
|
|
22
27
|
identityLinks: [],
|
|
28
|
+
identityClaims: /* @__PURE__ */ new Map(),
|
|
29
|
+
identityStaffAssistEnabled: false,
|
|
30
|
+
identitySuppressClaimDelivery: false,
|
|
31
|
+
identityAllowedReturnOrigins: /* @__PURE__ */ new Set([APP_TEST_ORIGIN]),
|
|
23
32
|
checkouts: [],
|
|
24
33
|
checkoutCounter: 0,
|
|
25
34
|
sessionStatuses: /* @__PURE__ */ new Map(),
|
|
@@ -47,6 +56,9 @@ var state = globalSingleton("state", () => ({
|
|
|
47
56
|
edgeBindings: /* @__PURE__ */ new Map(),
|
|
48
57
|
edgeCapabilities: null,
|
|
49
58
|
edgePeripheralCapabilities: /* @__PURE__ */ new Map(),
|
|
59
|
+
starlensCalls: [],
|
|
60
|
+
starlensReplies: [],
|
|
61
|
+
starlensDefaultReply: null,
|
|
50
62
|
allowNetwork: false
|
|
51
63
|
}));
|
|
52
64
|
|