@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/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 [existing] = matchingLinks(kind, externalId);
868
- if (existing) {
869
- if (existing.identityId === identityId) return success({ link: existing });
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 = matchingLinks(normalized.kind, normalized.externalId);
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
- if (canonicalIds.size !== 1) {
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 canonicalId = [...canonicalIds][0];
901
- const hasVerifiedLink = links.some((link) => link.verified);
902
- if (provenance === "guest" && hasVerifiedLink) {
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 [existing] = matchingLinks(kind, externalId);
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
- if (otherLinks.some((otherLink) => normalizedLinkKey(otherLink) === key)) {
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(`http://localhost:3333${path}`);
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
- return !!(dataStoreMatch || isEmail || identitiesMatch || storeMatch || messagingMatch || edgeMatch);
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
- state.identities.clear();
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
- state.identities.clear();
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