@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.js
CHANGED
|
@@ -106,6 +106,31 @@ function globalSingleton(key, create) {
|
|
|
106
106
|
return holder[symbol];
|
|
107
107
|
}
|
|
108
108
|
|
|
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 APP_TEST_ORIGIN = "http://localhost:3333";
|
|
117
|
+
var TEST_ENV_DEFAULTS = {
|
|
118
|
+
CONTROL_PLANE_URL: CONTROL_PLANE_TEST_URL,
|
|
119
|
+
DEPLOYMENT_SECRET: "stardeck-test-deployment-secret",
|
|
120
|
+
ORGANIZATION_ID: "00000000-0000-4000-8000-00000000000a",
|
|
121
|
+
PROJECT_ID: "00000000-0000-4000-8000-00000000000b",
|
|
122
|
+
DEPLOYMENT_ID: "00000000-0000-4000-8000-00000000000c",
|
|
123
|
+
STORAGE_URL: STORAGE_TEST_URL
|
|
124
|
+
};
|
|
125
|
+
var DEFAULT_TEST_USER = {
|
|
126
|
+
id: "test-user-1",
|
|
127
|
+
email: "test-user@example.com",
|
|
128
|
+
name: "Test User",
|
|
129
|
+
role: "member",
|
|
130
|
+
permissions: []
|
|
131
|
+
};
|
|
132
|
+
var DEFAULT_SCHEMA_PATH = "./src/generated/data-store-schema.sql";
|
|
133
|
+
|
|
109
134
|
// src/state.ts
|
|
110
135
|
var state = globalSingleton("state", () => ({
|
|
111
136
|
db: null,
|
|
@@ -117,6 +142,10 @@ var state = globalSingleton("state", () => ({
|
|
|
117
142
|
emailCounter: 0,
|
|
118
143
|
identities: /* @__PURE__ */ new Map(),
|
|
119
144
|
identityLinks: [],
|
|
145
|
+
identityClaims: /* @__PURE__ */ new Map(),
|
|
146
|
+
identityStaffAssistEnabled: false,
|
|
147
|
+
identitySuppressClaimDelivery: false,
|
|
148
|
+
identityAllowedReturnOrigins: /* @__PURE__ */ new Set([APP_TEST_ORIGIN]),
|
|
120
149
|
checkouts: [],
|
|
121
150
|
checkoutCounter: 0,
|
|
122
151
|
sessionStatuses: /* @__PURE__ */ new Map(),
|
|
@@ -144,6 +173,9 @@ var state = globalSingleton("state", () => ({
|
|
|
144
173
|
edgeBindings: /* @__PURE__ */ new Map(),
|
|
145
174
|
edgeCapabilities: null,
|
|
146
175
|
edgePeripheralCapabilities: /* @__PURE__ */ new Map(),
|
|
176
|
+
starlensCalls: [],
|
|
177
|
+
starlensReplies: [],
|
|
178
|
+
starlensDefaultReply: null,
|
|
147
179
|
allowNetwork: false
|
|
148
180
|
}));
|
|
149
181
|
function requireDb() {
|
|
@@ -155,30 +187,6 @@ function requireDb() {
|
|
|
155
187
|
return state.db;
|
|
156
188
|
}
|
|
157
189
|
|
|
158
|
-
// src/constants.ts
|
|
159
|
-
var TEST_DOMAIN_SUFFIX = ".stardeck.test";
|
|
160
|
-
var CONTROL_PLANE_TEST_URL = "https://control-plane.stardeck.test";
|
|
161
|
-
var DATA_STORE_TEST_HOST = "db.stardeck.test";
|
|
162
|
-
var DATA_STORE_TEST_URL = `postgresql://test:test@${DATA_STORE_TEST_HOST}/main`;
|
|
163
|
-
var STORAGE_TEST_URL = "https://storage.stardeck.test";
|
|
164
|
-
var STORAGE_TEST_HOST = "storage.stardeck.test";
|
|
165
|
-
var TEST_ENV_DEFAULTS = {
|
|
166
|
-
CONTROL_PLANE_URL: CONTROL_PLANE_TEST_URL,
|
|
167
|
-
DEPLOYMENT_SECRET: "stardeck-test-deployment-secret",
|
|
168
|
-
ORGANIZATION_ID: "00000000-0000-4000-8000-00000000000a",
|
|
169
|
-
PROJECT_ID: "00000000-0000-4000-8000-00000000000b",
|
|
170
|
-
DEPLOYMENT_ID: "00000000-0000-4000-8000-00000000000c",
|
|
171
|
-
STORAGE_URL: STORAGE_TEST_URL
|
|
172
|
-
};
|
|
173
|
-
var DEFAULT_TEST_USER = {
|
|
174
|
-
id: "test-user-1",
|
|
175
|
-
email: "test-user@example.com",
|
|
176
|
-
name: "Test User",
|
|
177
|
-
role: "member",
|
|
178
|
-
permissions: []
|
|
179
|
-
};
|
|
180
|
-
var DEFAULT_SCHEMA_PATH = "./src/generated/data-store-schema.sql";
|
|
181
|
-
|
|
182
190
|
// src/simulator/hmac.ts
|
|
183
191
|
var import_node_crypto = __toESM(require("crypto"));
|
|
184
192
|
var TIMESTAMP_TOLERANCE_SECONDS = 300;
|
|
@@ -229,8 +237,8 @@ function json(body, status = 200) {
|
|
|
229
237
|
function success(data) {
|
|
230
238
|
return json({ success: true, data });
|
|
231
239
|
}
|
|
232
|
-
function failure(error, status = 400) {
|
|
233
|
-
return json({ success: false, error }, status);
|
|
240
|
+
function failure(error, status = 400, code) {
|
|
241
|
+
return json({ success: false, error, ...code ? { code } : {} }, status);
|
|
234
242
|
}
|
|
235
243
|
async function readJsonBody(request) {
|
|
236
244
|
try {
|
|
@@ -718,6 +726,12 @@ var TRUSTED_LINK_KINDS = /* @__PURE__ */ new Set([
|
|
|
718
726
|
"dashboard_user"
|
|
719
727
|
]);
|
|
720
728
|
var RESOLVE_KINDS = /* @__PURE__ */ new Set(["phone", "email", "line"]);
|
|
729
|
+
var CLAIM_KINDS = /* @__PURE__ */ new Set(["email", "phone"]);
|
|
730
|
+
var CLAIM_FLOWS = /* @__PURE__ */ new Set(["invite", "assist", "transfer"]);
|
|
731
|
+
var DUPLICABLE_UNVERIFIED_KINDS = /* @__PURE__ */ new Set(["email", "phone"]);
|
|
732
|
+
var CLAIM_TTL_MS = 48 * 60 * 60 * 1e3;
|
|
733
|
+
var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
734
|
+
var STAFF_ASSISTANCE_NOT_ENABLED = "STAFF_ASSISTANCE_NOT_ENABLED";
|
|
721
735
|
var E164_PHONE = /^\+[1-9]\d{1,14}$/;
|
|
722
736
|
var SEARCH_DEFAULT_LIMIT = 50;
|
|
723
737
|
var SEARCH_MAX_LIMIT = 100;
|
|
@@ -800,6 +814,11 @@ function matchingLinks(kind, externalId) {
|
|
|
800
814
|
(link) => link.kind === kind && comparableExternalId(link.kind, link.externalId) === comparable
|
|
801
815
|
);
|
|
802
816
|
}
|
|
817
|
+
function sortedMatchingLinks(kind, externalId) {
|
|
818
|
+
return matchingLinks(kind, externalId).sort(
|
|
819
|
+
(a, b) => Number(b.verified) - Number(a.verified) || a.createdAt.localeCompare(b.createdAt) || state.identityLinks.indexOf(a) - state.identityLinks.indexOf(b)
|
|
820
|
+
);
|
|
821
|
+
}
|
|
803
822
|
function normalizedLinkKey(link) {
|
|
804
823
|
return `${link.kind}:${comparableExternalId(link.kind, link.externalId)}`;
|
|
805
824
|
}
|
|
@@ -913,9 +932,10 @@ async function handleAttachLink(identityId, request) {
|
|
|
913
932
|
);
|
|
914
933
|
if ("error" in normalized) return failure(normalized.error);
|
|
915
934
|
const { kind, externalId } = normalized;
|
|
916
|
-
const
|
|
917
|
-
|
|
918
|
-
|
|
935
|
+
const matches = matchingLinks(kind, externalId);
|
|
936
|
+
const ownRow = matches.find((link2) => link2.identityId === identityId);
|
|
937
|
+
if (ownRow) return success({ link: ownRow });
|
|
938
|
+
if (!DUPLICABLE_UNVERIFIED_KINDS.has(kind) && matches.length > 0) {
|
|
919
939
|
return failure("identifier already linked to another identity", 409);
|
|
920
940
|
}
|
|
921
941
|
const link = createUnverifiedLink(identityId, kind, externalId);
|
|
@@ -932,7 +952,7 @@ async function handleResolve(request) {
|
|
|
932
952
|
if (body.displayName !== void 0 && typeof body.displayName !== "string") {
|
|
933
953
|
return failure("displayName must be a string");
|
|
934
954
|
}
|
|
935
|
-
const links =
|
|
955
|
+
const links = sortedMatchingLinks(normalized.kind, normalized.externalId);
|
|
936
956
|
if (links.length > 0) {
|
|
937
957
|
const canonicalIds = /* @__PURE__ */ new Set();
|
|
938
958
|
for (const link of links) {
|
|
@@ -943,12 +963,15 @@ async function handleResolve(request) {
|
|
|
943
963
|
return failure(`cannot resolve identity link: ${message}`, 409);
|
|
944
964
|
}
|
|
945
965
|
}
|
|
946
|
-
|
|
966
|
+
const verifiedOwners = new Set(
|
|
967
|
+
links.filter((link) => link.verified).map((link) => link.identityId)
|
|
968
|
+
);
|
|
969
|
+
if (verifiedOwners.size > 1 || !DUPLICABLE_UNVERIFIED_KINDS.has(normalized.kind) && canonicalIds.size !== 1) {
|
|
947
970
|
return failure("identifier is linked to multiple identities", 409);
|
|
948
971
|
}
|
|
949
|
-
const
|
|
950
|
-
const
|
|
951
|
-
if (provenance === "guest" &&
|
|
972
|
+
const [chosen] = links;
|
|
973
|
+
const canonicalId = canonicalIdentity(chosen.identityId).id;
|
|
974
|
+
if (provenance === "guest" && chosen.verified) {
|
|
952
975
|
return success({ identityId: null, reason: "verified_conflict" });
|
|
953
976
|
}
|
|
954
977
|
return success({ identityId: canonicalId, created: false });
|
|
@@ -1050,6 +1073,116 @@ async function handleAliases(request) {
|
|
|
1050
1073
|
}
|
|
1051
1074
|
return success({ aliases });
|
|
1052
1075
|
}
|
|
1076
|
+
function claimView(claim) {
|
|
1077
|
+
const state_ = claim.state === "pending" && Date.parse(claim.expiresAt) <= Date.now() ? "expired" : claim.state;
|
|
1078
|
+
return {
|
|
1079
|
+
claimId: claim.claimId,
|
|
1080
|
+
flow: claim.flow,
|
|
1081
|
+
state: state_,
|
|
1082
|
+
refusalReason: claim.refusalReason,
|
|
1083
|
+
completedIdentityId: claim.completedIdentityId,
|
|
1084
|
+
expiresAt: claim.expiresAt
|
|
1085
|
+
};
|
|
1086
|
+
}
|
|
1087
|
+
function validateClaimStaff(flow, staff) {
|
|
1088
|
+
if (flow === "invite") {
|
|
1089
|
+
if (staff !== void 0) return { error: "staff is only accepted for assist or transfer" };
|
|
1090
|
+
return { staffActorRef: null };
|
|
1091
|
+
}
|
|
1092
|
+
if (!staff || typeof staff !== "object") {
|
|
1093
|
+
return { error: `staff is required for a ${flow} claim` };
|
|
1094
|
+
}
|
|
1095
|
+
const { actorRef, grant } = staff;
|
|
1096
|
+
if (typeof actorRef !== "string" || actorRef.length === 0) {
|
|
1097
|
+
return { error: "staff.actorRef is required" };
|
|
1098
|
+
}
|
|
1099
|
+
if (grant !== flow) return { error: `staff.grant must be '${flow}' for a ${flow} claim` };
|
|
1100
|
+
return { staffActorRef: actorRef };
|
|
1101
|
+
}
|
|
1102
|
+
async function handleCreateClaim(request) {
|
|
1103
|
+
const body = await readJsonBody(request);
|
|
1104
|
+
const flow = body.flow;
|
|
1105
|
+
if (typeof flow !== "string" || !CLAIM_FLOWS.has(flow)) {
|
|
1106
|
+
return failure("flow must be one of: invite, assist, transfer");
|
|
1107
|
+
}
|
|
1108
|
+
if (typeof body.identityId !== "string" || !UUID.test(body.identityId)) {
|
|
1109
|
+
return failure("identityId must be a uuid");
|
|
1110
|
+
}
|
|
1111
|
+
const normalized = normalizeLinkInput(
|
|
1112
|
+
body.kind,
|
|
1113
|
+
body.externalId,
|
|
1114
|
+
CLAIM_KINDS,
|
|
1115
|
+
"kind must be one of: email, phone"
|
|
1116
|
+
);
|
|
1117
|
+
if ("error" in normalized) return failure(normalized.error);
|
|
1118
|
+
let returnTo = null;
|
|
1119
|
+
if (body.returnTo !== void 0) {
|
|
1120
|
+
if (typeof body.returnTo !== "string") return failure("returnTo must be a string");
|
|
1121
|
+
let parsed;
|
|
1122
|
+
try {
|
|
1123
|
+
parsed = new URL(body.returnTo);
|
|
1124
|
+
} catch {
|
|
1125
|
+
return failure("returnTo must be an absolute URL");
|
|
1126
|
+
}
|
|
1127
|
+
returnTo = state.identityAllowedReturnOrigins.has(parsed.origin) ? body.returnTo : null;
|
|
1128
|
+
}
|
|
1129
|
+
if (body.deliver !== void 0 && typeof body.deliver !== "boolean") {
|
|
1130
|
+
return failure("deliver must be a boolean");
|
|
1131
|
+
}
|
|
1132
|
+
const staff = validateClaimStaff(flow, body.staff);
|
|
1133
|
+
if ("error" in staff) return failure(staff.error);
|
|
1134
|
+
if (flow !== "invite" && !state.identityStaffAssistEnabled) {
|
|
1135
|
+
return failure(
|
|
1136
|
+
"staff-assisted profile claiming is not enabled for this project",
|
|
1137
|
+
403,
|
|
1138
|
+
STAFF_ASSISTANCE_NOT_ENABLED
|
|
1139
|
+
);
|
|
1140
|
+
}
|
|
1141
|
+
const identity = state.identities.get(body.identityId);
|
|
1142
|
+
if (!identity || identity.type !== "person" || identity.status !== "active") {
|
|
1143
|
+
return failure("identity not found", 404);
|
|
1144
|
+
}
|
|
1145
|
+
const holdsContact = matchingLinks(normalized.kind, normalized.externalId).some(
|
|
1146
|
+
(link) => link.identityId === identity.id
|
|
1147
|
+
);
|
|
1148
|
+
if (!holdsContact) {
|
|
1149
|
+
return failure("identity does not hold that contact", 409);
|
|
1150
|
+
}
|
|
1151
|
+
const claimId = import_node_crypto3.default.randomUUID();
|
|
1152
|
+
const deliver = body.deliver === true;
|
|
1153
|
+
const handle = import_node_crypto3.default.randomBytes(32).toString("base64url");
|
|
1154
|
+
const claim = {
|
|
1155
|
+
claimId,
|
|
1156
|
+
flow,
|
|
1157
|
+
identityId: identity.id,
|
|
1158
|
+
kind: normalized.kind,
|
|
1159
|
+
externalId: normalized.externalId,
|
|
1160
|
+
returnTo,
|
|
1161
|
+
staffActorRef: staff.staffActorRef,
|
|
1162
|
+
state: "pending",
|
|
1163
|
+
refusalReason: null,
|
|
1164
|
+
completedIdentityId: null,
|
|
1165
|
+
claimantClerkUserId: null,
|
|
1166
|
+
claimUrl: `${CONTROL_PLANE_TEST_URL}/claim/${handle}`,
|
|
1167
|
+
delivered: deliver ? !state.identitySuppressClaimDelivery : null,
|
|
1168
|
+
expiresAt: new Date(Date.now() + CLAIM_TTL_MS).toISOString(),
|
|
1169
|
+
createdAt: now()
|
|
1170
|
+
};
|
|
1171
|
+
state.identityClaims.set(claimId, claim);
|
|
1172
|
+
return success({
|
|
1173
|
+
claimId,
|
|
1174
|
+
claimUrl: claim.claimUrl,
|
|
1175
|
+
expiresAt: claim.expiresAt,
|
|
1176
|
+
// Absent unless asked for: an app delivering its own link must not read a
|
|
1177
|
+
// missing field as "nothing was sent".
|
|
1178
|
+
...deliver ? { delivered: claim.delivered } : {}
|
|
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
|
+
}
|
|
1053
1186
|
async function handleIdentitiesRequest(request, subPath) {
|
|
1054
1187
|
const method = request.method;
|
|
1055
1188
|
if (subPath === "" || subPath === "/") {
|
|
@@ -1065,6 +1198,13 @@ async function handleIdentitiesRequest(request, subPath) {
|
|
|
1065
1198
|
if (subPath === "/search" && method === "GET") {
|
|
1066
1199
|
return handleSearch(request);
|
|
1067
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
|
+
}
|
|
1068
1208
|
const linksMatch = subPath.match(/^\/([^/]+)\/links$/);
|
|
1069
1209
|
if (linksMatch && method === "POST") {
|
|
1070
1210
|
return handleAttachLink(linksMatch[1], request);
|
|
@@ -1089,11 +1229,13 @@ function seedVerifiedLink(identityId, params) {
|
|
|
1089
1229
|
);
|
|
1090
1230
|
if ("error" in normalized) throw new Error(normalized.error);
|
|
1091
1231
|
const { kind, externalId } = normalized;
|
|
1092
|
-
const
|
|
1232
|
+
const matches = matchingLinks(kind, externalId);
|
|
1233
|
+
const blocker = matches.find(
|
|
1234
|
+
(link2) => link2.identityId !== identityId && (link2.verified || !DUPLICABLE_UNVERIFIED_KINDS.has(link2.kind))
|
|
1235
|
+
);
|
|
1236
|
+
if (blocker) throw new Error("identifier already linked to another identity");
|
|
1237
|
+
const existing = matches.find((link2) => link2.identityId === identityId);
|
|
1093
1238
|
if (existing) {
|
|
1094
|
-
if (existing.identityId !== identityId) {
|
|
1095
|
-
throw new Error("identifier already linked to another identity");
|
|
1096
|
-
}
|
|
1097
1239
|
existing.verified = true;
|
|
1098
1240
|
return existing;
|
|
1099
1241
|
}
|
|
@@ -1123,7 +1265,12 @@ function merge(sourceIdentityId, canonicalIdentityId) {
|
|
|
1123
1265
|
);
|
|
1124
1266
|
for (const sourceLink of sourceLinks) {
|
|
1125
1267
|
const key = normalizedLinkKey(sourceLink);
|
|
1126
|
-
|
|
1268
|
+
const conflicting = otherLinks.some(
|
|
1269
|
+
(otherLink) => normalizedLinkKey(otherLink) === key && // Same partial rule as the index: a third identity's UNVERIFIED copy of a
|
|
1270
|
+
// shared email/phone is allowed to coexist and must not block the merge.
|
|
1271
|
+
(!DUPLICABLE_UNVERIFIED_KINDS.has(key.split(":")[0]) || otherLink.verified && sourceLink.verified)
|
|
1272
|
+
);
|
|
1273
|
+
if (conflicting) {
|
|
1127
1274
|
throw new Error("merge would conflict with a link owned by another identity");
|
|
1128
1275
|
}
|
|
1129
1276
|
}
|
|
@@ -1150,6 +1297,45 @@ function merge(sourceIdentityId, canonicalIdentityId) {
|
|
|
1150
1297
|
source.mergedIntoId = canonical.id;
|
|
1151
1298
|
source.updatedAt = now();
|
|
1152
1299
|
}
|
|
1300
|
+
function requireClaim(claimId) {
|
|
1301
|
+
const claim = state.identityClaims.get(claimId);
|
|
1302
|
+
if (!claim) throw new Error("claim not found");
|
|
1303
|
+
return claim;
|
|
1304
|
+
}
|
|
1305
|
+
function completeClaim(claimId, params) {
|
|
1306
|
+
const claim = requireClaim(claimId);
|
|
1307
|
+
if (claim.state !== "pending") return claim;
|
|
1308
|
+
if (Date.parse(claim.expiresAt) <= Date.now()) {
|
|
1309
|
+
claim.state = "expired";
|
|
1310
|
+
return claim;
|
|
1311
|
+
}
|
|
1312
|
+
if (!params.clerkUserId) throw new Error("clerkUserId is required");
|
|
1313
|
+
const rows = matchingLinks(claim.kind, claim.externalId);
|
|
1314
|
+
const target = rows.find((link) => link.identityId === claim.identityId);
|
|
1315
|
+
if (!target) {
|
|
1316
|
+
claim.state = "invalidated";
|
|
1317
|
+
return claim;
|
|
1318
|
+
}
|
|
1319
|
+
const otherVerified = rows.find((link) => link.verified && link.identityId !== claim.identityId);
|
|
1320
|
+
if (otherVerified) {
|
|
1321
|
+
claim.state = "refused";
|
|
1322
|
+
claim.refusalReason = "contact_owned";
|
|
1323
|
+
return claim;
|
|
1324
|
+
}
|
|
1325
|
+
target.verified = true;
|
|
1326
|
+
claim.state = "completed";
|
|
1327
|
+
claim.completedIdentityId = claim.identityId;
|
|
1328
|
+
claim.claimantClerkUserId = params.clerkUserId;
|
|
1329
|
+
return claim;
|
|
1330
|
+
}
|
|
1331
|
+
function refuseClaim(claimId, reason) {
|
|
1332
|
+
const claim = requireClaim(claimId);
|
|
1333
|
+
if (claim.state !== "pending") return claim;
|
|
1334
|
+
if (!reason) throw new Error("a refusal reason is required");
|
|
1335
|
+
claim.state = "refused";
|
|
1336
|
+
claim.refusalReason = reason;
|
|
1337
|
+
return claim;
|
|
1338
|
+
}
|
|
1153
1339
|
function createDirectory() {
|
|
1154
1340
|
return {
|
|
1155
1341
|
all: () => [...state.identities.values()],
|
|
@@ -1157,9 +1343,26 @@ function createDirectory() {
|
|
|
1157
1343
|
links: (identityId) => linksFor(identityId),
|
|
1158
1344
|
seedVerifiedLink,
|
|
1159
1345
|
merge,
|
|
1346
|
+
claims: () => [...state.identityClaims.values()],
|
|
1347
|
+
claim: (claimId) => state.identityClaims.get(claimId),
|
|
1348
|
+
enableStaffAssist: (enabled = true) => {
|
|
1349
|
+
state.identityStaffAssistEnabled = enabled;
|
|
1350
|
+
},
|
|
1351
|
+
suppressClaimDelivery: (enabled = true) => {
|
|
1352
|
+
state.identitySuppressClaimDelivery = enabled;
|
|
1353
|
+
},
|
|
1354
|
+
allowReturnOrigin: (origin) => {
|
|
1355
|
+
state.identityAllowedReturnOrigins.add(new URL(origin).origin);
|
|
1356
|
+
},
|
|
1357
|
+
completeClaim,
|
|
1358
|
+
refuseClaim,
|
|
1160
1359
|
clear: () => {
|
|
1161
1360
|
state.identities.clear();
|
|
1162
1361
|
state.identityLinks = [];
|
|
1362
|
+
state.identityClaims.clear();
|
|
1363
|
+
state.identityStaffAssistEnabled = false;
|
|
1364
|
+
state.identitySuppressClaimDelivery = false;
|
|
1365
|
+
state.identityAllowedReturnOrigins = /* @__PURE__ */ new Set([APP_TEST_ORIGIN]);
|
|
1163
1366
|
},
|
|
1164
1367
|
get count() {
|
|
1165
1368
|
return state.identities.size;
|
|
@@ -1198,7 +1401,7 @@ async function importNextServer() {
|
|
|
1198
1401
|
async function callRoute(handler, options = {}) {
|
|
1199
1402
|
const { NextRequest } = await importNextServer();
|
|
1200
1403
|
const path = options.path ?? "/api/test-route";
|
|
1201
|
-
const url = new URL(
|
|
1404
|
+
const url = new URL(`${APP_TEST_ORIGIN}${path}`);
|
|
1202
1405
|
for (const [key, value] of Object.entries(options.searchParams ?? {})) {
|
|
1203
1406
|
url.searchParams.set(key, value);
|
|
1204
1407
|
}
|
|
@@ -2553,6 +2756,104 @@ function createEdge() {
|
|
|
2553
2756
|
};
|
|
2554
2757
|
}
|
|
2555
2758
|
|
|
2759
|
+
// src/simulator/starlens.ts
|
|
2760
|
+
function starlensFailure(error, status, code) {
|
|
2761
|
+
return json({ success: false, error, code }, status);
|
|
2762
|
+
}
|
|
2763
|
+
function nullFlagged(output, issues) {
|
|
2764
|
+
const copy = JSON.parse(JSON.stringify(output ?? null));
|
|
2765
|
+
for (const { path } of issues) {
|
|
2766
|
+
const keys = path.replace(/^output\./, "").replace(/\[(\d+)\]/g, ".$1").split(".");
|
|
2767
|
+
const last = keys.pop();
|
|
2768
|
+
const parent = keys.reduce(
|
|
2769
|
+
(o, k) => o != null && typeof o === "object" ? o[k] : void 0,
|
|
2770
|
+
copy
|
|
2771
|
+
);
|
|
2772
|
+
if (parent != null && typeof parent === "object" && Object.prototype.hasOwnProperty.call(parent, last)) {
|
|
2773
|
+
parent[last] = null;
|
|
2774
|
+
}
|
|
2775
|
+
}
|
|
2776
|
+
return copy;
|
|
2777
|
+
}
|
|
2778
|
+
async function handleStarlensExtract(request, signedDeploymentId) {
|
|
2779
|
+
const body = await readJsonBody(request);
|
|
2780
|
+
if (signedDeploymentId !== null && body.deploymentId !== signedDeploymentId) {
|
|
2781
|
+
return starlensFailure("Deployment ID mismatch", 401, "AUTHENTICATION_ERROR");
|
|
2782
|
+
}
|
|
2783
|
+
const inputs = Array.isArray(body.inputs) ? body.inputs : [];
|
|
2784
|
+
if (inputs.length === 0) {
|
|
2785
|
+
return starlensFailure(
|
|
2786
|
+
"Validation failed \u2014 inputs: at least one input",
|
|
2787
|
+
400,
|
|
2788
|
+
"VALIDATION_ERROR"
|
|
2789
|
+
);
|
|
2790
|
+
}
|
|
2791
|
+
const schema = body.schema;
|
|
2792
|
+
if (!schema || schema.type !== "object" || typeof schema.properties !== "object") {
|
|
2793
|
+
return starlensFailure(
|
|
2794
|
+
"Validation failed \u2014 schema.properties is required",
|
|
2795
|
+
400,
|
|
2796
|
+
"VALIDATION_ERROR"
|
|
2797
|
+
);
|
|
2798
|
+
}
|
|
2799
|
+
const call = {
|
|
2800
|
+
version: typeof body.version === "string" ? body.version : "latest",
|
|
2801
|
+
inputs: inputs.map((i) => ({
|
|
2802
|
+
name: typeof i.filename === "string" ? i.filename : typeof i.url === "string" ? i.url : "upload",
|
|
2803
|
+
mediaType: typeof i.mediaType === "string" ? i.mediaType : null,
|
|
2804
|
+
source: typeof i.url === "string" ? "url" : "data"
|
|
2805
|
+
})),
|
|
2806
|
+
schema,
|
|
2807
|
+
instructions: typeof body.instructions === "string" ? body.instructions : void 0
|
|
2808
|
+
};
|
|
2809
|
+
state.starlensCalls.push(call);
|
|
2810
|
+
const reply = state.starlensReplies.shift() ?? state.starlensDefaultReply;
|
|
2811
|
+
if (!reply) {
|
|
2812
|
+
return starlensFailure(
|
|
2813
|
+
"[stardeck-testing] No Starlens reply registered. Call app.starlens.mock({ output, issues }) or app.starlens.reject(reason) before the request.",
|
|
2814
|
+
502,
|
|
2815
|
+
"model_failed"
|
|
2816
|
+
);
|
|
2817
|
+
}
|
|
2818
|
+
if ("reject" in reply) {
|
|
2819
|
+
return starlensFailure(reply.reject, 422, "unsupported_document");
|
|
2820
|
+
}
|
|
2821
|
+
const issues = reply.issues ?? [];
|
|
2822
|
+
return success({
|
|
2823
|
+
version: call.version === "latest" ? "v1" : call.version,
|
|
2824
|
+
output: nullFlagged(reply.output, issues),
|
|
2825
|
+
issues,
|
|
2826
|
+
usage: { inputTokens: 0, outputTokens: 0, cost: 0 }
|
|
2827
|
+
});
|
|
2828
|
+
}
|
|
2829
|
+
function createStarlens() {
|
|
2830
|
+
return {
|
|
2831
|
+
mock(reply) {
|
|
2832
|
+
state.starlensReplies.push(reply);
|
|
2833
|
+
},
|
|
2834
|
+
reject(reason) {
|
|
2835
|
+
state.starlensReplies.push({ reject: reason });
|
|
2836
|
+
},
|
|
2837
|
+
mockDefault(reply) {
|
|
2838
|
+
state.starlensDefaultReply = reply;
|
|
2839
|
+
},
|
|
2840
|
+
get calls() {
|
|
2841
|
+
return state.starlensCalls;
|
|
2842
|
+
},
|
|
2843
|
+
latestCall() {
|
|
2844
|
+
return state.starlensCalls[state.starlensCalls.length - 1];
|
|
2845
|
+
},
|
|
2846
|
+
clear() {
|
|
2847
|
+
state.starlensCalls = [];
|
|
2848
|
+
state.starlensReplies = [];
|
|
2849
|
+
state.starlensDefaultReply = null;
|
|
2850
|
+
},
|
|
2851
|
+
get count() {
|
|
2852
|
+
return state.starlensCalls.length;
|
|
2853
|
+
}
|
|
2854
|
+
};
|
|
2855
|
+
}
|
|
2856
|
+
|
|
2556
2857
|
// src/simulator/router.ts
|
|
2557
2858
|
var fetchHolder = globalSingleton("fetch-holder", () => ({
|
|
2558
2859
|
originalFetch: null
|
|
@@ -2574,7 +2875,8 @@ function requiresDeploymentHmac(request, url) {
|
|
|
2574
2875
|
/^\/api\/deployments\/[^/]+\/integrations\/(slack|line|facebook)(\/.*)?$/
|
|
2575
2876
|
);
|
|
2576
2877
|
const edgeMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/edge(\/.*)?$/);
|
|
2577
|
-
|
|
2878
|
+
const isStarlens = url.pathname === "/api/starlens/extract";
|
|
2879
|
+
return !!(isStarlens || dataStoreMatch || isEmail || identitiesMatch || storeMatch || messagingMatch || edgeMatch);
|
|
2578
2880
|
}
|
|
2579
2881
|
function deploymentIdFromPath(url) {
|
|
2580
2882
|
const match = url.pathname.match(/\/api\/deployments\/([^/]+)(?:\/|$)/);
|
|
@@ -2604,6 +2906,7 @@ async function handleSimulatedRequest(request, url) {
|
|
|
2604
2906
|
);
|
|
2605
2907
|
const edgeMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/edge(\/.*)?$/);
|
|
2606
2908
|
const isStorageHost = url.hostname === STORAGE_TEST_HOST;
|
|
2909
|
+
let signedDeploymentId = null;
|
|
2607
2910
|
if (requiresDeploymentHmac(request, url)) {
|
|
2608
2911
|
const authHeader = request.headers.get("X-Stardeck-Auth");
|
|
2609
2912
|
if (!authHeader) {
|
|
@@ -2615,6 +2918,7 @@ async function handleSimulatedRequest(request, url) {
|
|
|
2615
2918
|
if (!authPayload || pathDeploymentId !== null && authPayload.deploymentId !== pathDeploymentId) {
|
|
2616
2919
|
return failure("Invalid authentication", 401);
|
|
2617
2920
|
}
|
|
2921
|
+
signedDeploymentId = authPayload.deploymentId;
|
|
2618
2922
|
}
|
|
2619
2923
|
if (isStorageHost) {
|
|
2620
2924
|
return handleStorageRequest(request, url);
|
|
@@ -2635,6 +2939,9 @@ async function handleSimulatedRequest(request, url) {
|
|
|
2635
2939
|
if (edgeMatch) {
|
|
2636
2940
|
return handleEdgeRequest(request, edgeMatch[1] ?? "");
|
|
2637
2941
|
}
|
|
2942
|
+
if (url.pathname === "/api/starlens/extract" && request.method === "POST") {
|
|
2943
|
+
return handleStarlensExtract(request, signedDeploymentId);
|
|
2944
|
+
}
|
|
2638
2945
|
if (dataStoreMatch) {
|
|
2639
2946
|
const subPath = dataStoreMatch[1] ?? "";
|
|
2640
2947
|
const db = requireDb();
|
|
@@ -2656,7 +2963,7 @@ async function handleSimulatedRequest(request, url) {
|
|
|
2656
2963
|
}
|
|
2657
2964
|
}
|
|
2658
2965
|
return failure(
|
|
2659
|
-
`[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.`,
|
|
2966
|
+
`[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.`,
|
|
2660
2967
|
404
|
|
2661
2968
|
);
|
|
2662
2969
|
}
|
|
@@ -2824,6 +3131,7 @@ async function createTestApp(options = {}) {
|
|
|
2824
3131
|
const storage = createStorage();
|
|
2825
3132
|
const messages = createMessages();
|
|
2826
3133
|
const edge = createEdge();
|
|
3134
|
+
const starlens = createStarlens();
|
|
2827
3135
|
const app = {
|
|
2828
3136
|
db,
|
|
2829
3137
|
inbox,
|
|
@@ -2832,6 +3140,7 @@ async function createTestApp(options = {}) {
|
|
|
2832
3140
|
storage,
|
|
2833
3141
|
messages,
|
|
2834
3142
|
edge,
|
|
3143
|
+
starlens,
|
|
2835
3144
|
async query(sql, params = []) {
|
|
2836
3145
|
const result = await db.query(sql, params);
|
|
2837
3146
|
return result.rows;
|
|
@@ -2867,12 +3176,12 @@ async function createTestApp(options = {}) {
|
|
|
2867
3176
|
state.refreshSessions.clear();
|
|
2868
3177
|
state.emails = [];
|
|
2869
3178
|
state.emailCounter = 0;
|
|
2870
|
-
|
|
2871
|
-
state.identityLinks = [];
|
|
3179
|
+
directory.clear();
|
|
2872
3180
|
payments.clear();
|
|
2873
3181
|
storage.clear();
|
|
2874
3182
|
messages.clear();
|
|
2875
3183
|
edge.clear();
|
|
3184
|
+
starlens.clear();
|
|
2876
3185
|
},
|
|
2877
3186
|
async close() {
|
|
2878
3187
|
state.db = null;
|
|
@@ -2881,12 +3190,12 @@ async function createTestApp(options = {}) {
|
|
|
2881
3190
|
state.refreshSessions.clear();
|
|
2882
3191
|
state.emails = [];
|
|
2883
3192
|
state.emailCounter = 0;
|
|
2884
|
-
|
|
2885
|
-
state.identityLinks = [];
|
|
3193
|
+
directory.clear();
|
|
2886
3194
|
payments.clear();
|
|
2887
3195
|
storage.clear();
|
|
2888
3196
|
messages.clear();
|
|
2889
3197
|
edge.clear();
|
|
3198
|
+
starlens.clear();
|
|
2890
3199
|
uninstallFetchRouter();
|
|
2891
3200
|
await db.close();
|
|
2892
3201
|
}
|