@stardeck-customer-apps/testing 0.1.1 → 0.3.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 +7 -0
- package/dist/index.d.mts +91 -1
- package/dist/index.d.ts +91 -1
- package/dist/index.js +207 -12
- package/dist/index.mjs +209 -12
- package/dist/next/headers-shim.js +2 -0
- package/dist/next/headers-shim.mjs +2 -0
- package/dist/setup.js +149 -9
- package/dist/setup.mjs +149 -9
- package/package.json +8 -2
package/dist/setup.mjs
CHANGED
|
@@ -14,6 +14,8 @@ var state = globalSingleton("state", () => ({
|
|
|
14
14
|
refreshSessions: /* @__PURE__ */ new Map(),
|
|
15
15
|
emails: [],
|
|
16
16
|
emailCounter: 0,
|
|
17
|
+
identities: /* @__PURE__ */ new Map(),
|
|
18
|
+
identityLinks: [],
|
|
17
19
|
allowNetwork: false
|
|
18
20
|
}));
|
|
19
21
|
function requireDb() {
|
|
@@ -65,8 +67,8 @@ function verifyDeploymentAuthHeader(secret, header) {
|
|
|
65
67
|
return null;
|
|
66
68
|
}
|
|
67
69
|
if (payload.type !== "deployment-request") return null;
|
|
68
|
-
const
|
|
69
|
-
if (Math.abs(
|
|
70
|
+
const now2 = Math.floor(Date.now() / 1e3);
|
|
71
|
+
if (Math.abs(now2 - payload.timestamp) > TIMESTAMP_TOLERANCE_SECONDS) return null;
|
|
70
72
|
return payload;
|
|
71
73
|
}
|
|
72
74
|
|
|
@@ -370,6 +372,7 @@ var FIELD_TYPE_TO_PG = {
|
|
|
370
372
|
currency: "numeric",
|
|
371
373
|
rating: "integer",
|
|
372
374
|
relation: "uuid",
|
|
375
|
+
identity: "uuid",
|
|
373
376
|
file_ref: "jsonb",
|
|
374
377
|
file_refs: "jsonb",
|
|
375
378
|
json: "jsonb"
|
|
@@ -545,6 +548,139 @@ async function handleEmailSend(request) {
|
|
|
545
548
|
return success({ resendId, fromAddress });
|
|
546
549
|
}
|
|
547
550
|
|
|
551
|
+
// src/simulator/identities.ts
|
|
552
|
+
import crypto3 from "crypto";
|
|
553
|
+
var LINK_KINDS = /* @__PURE__ */ new Set([
|
|
554
|
+
"line",
|
|
555
|
+
"facebook",
|
|
556
|
+
"instagram",
|
|
557
|
+
"email",
|
|
558
|
+
"phone",
|
|
559
|
+
"project_auth_user",
|
|
560
|
+
"dashboard_user"
|
|
561
|
+
]);
|
|
562
|
+
function now() {
|
|
563
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
564
|
+
}
|
|
565
|
+
function linksFor(identityId) {
|
|
566
|
+
return state.identityLinks.filter((l) => l.identityId === identityId);
|
|
567
|
+
}
|
|
568
|
+
async function readBody(request) {
|
|
569
|
+
try {
|
|
570
|
+
return await request.json();
|
|
571
|
+
} catch {
|
|
572
|
+
return {};
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
function handleList(request) {
|
|
576
|
+
const typeParam = new URL(request.url).searchParams.get("type");
|
|
577
|
+
const type = typeParam === "person" || typeParam === "account" ? typeParam : void 0;
|
|
578
|
+
const identities = [...state.identities.values()];
|
|
579
|
+
return success({ identities: type ? identities.filter((i) => i.type === type) : identities });
|
|
580
|
+
}
|
|
581
|
+
async function handleCreate(request) {
|
|
582
|
+
const body = await readBody(request);
|
|
583
|
+
const type = body.type;
|
|
584
|
+
if (type !== "person" && type !== "account") {
|
|
585
|
+
return failure("type must be 'person' or 'account'");
|
|
586
|
+
}
|
|
587
|
+
let parentId = null;
|
|
588
|
+
if (type === "person" && body.parentId) {
|
|
589
|
+
const parent = state.identities.get(body.parentId);
|
|
590
|
+
if (!parent) return failure("parent identity not found");
|
|
591
|
+
if (parent.type !== "account") {
|
|
592
|
+
return failure("a person's parent must be an account");
|
|
593
|
+
}
|
|
594
|
+
parentId = parent.id;
|
|
595
|
+
}
|
|
596
|
+
const identity = {
|
|
597
|
+
id: crypto3.randomUUID(),
|
|
598
|
+
type,
|
|
599
|
+
parentId,
|
|
600
|
+
displayName: body.displayName ?? null,
|
|
601
|
+
profile: body.profile ?? {},
|
|
602
|
+
status: "active",
|
|
603
|
+
mergedIntoId: null,
|
|
604
|
+
externalRef: body.externalRef ?? null,
|
|
605
|
+
createdAt: now(),
|
|
606
|
+
updatedAt: now()
|
|
607
|
+
};
|
|
608
|
+
state.identities.set(identity.id, identity);
|
|
609
|
+
return success({ identity });
|
|
610
|
+
}
|
|
611
|
+
function handleGet(identityId) {
|
|
612
|
+
const identity = state.identities.get(identityId);
|
|
613
|
+
if (!identity) return failure("identity not found", 404);
|
|
614
|
+
return success({ identity, links: linksFor(identityId) });
|
|
615
|
+
}
|
|
616
|
+
async function handleUpdate(identityId, request) {
|
|
617
|
+
const identity = state.identities.get(identityId);
|
|
618
|
+
if (!identity) return failure("identity not found", 404);
|
|
619
|
+
const body = await readBody(request);
|
|
620
|
+
if (body.displayName !== void 0) {
|
|
621
|
+
identity.displayName = body.displayName;
|
|
622
|
+
}
|
|
623
|
+
if (body.profile !== void 0) {
|
|
624
|
+
identity.profile = body.profile;
|
|
625
|
+
}
|
|
626
|
+
if (body.externalRef !== void 0) {
|
|
627
|
+
identity.externalRef = body.externalRef;
|
|
628
|
+
}
|
|
629
|
+
identity.updatedAt = now();
|
|
630
|
+
return success({ identity });
|
|
631
|
+
}
|
|
632
|
+
async function handleAttachLink(identityId, request) {
|
|
633
|
+
const identity = state.identities.get(identityId);
|
|
634
|
+
if (!identity) return failure("identity not found", 404);
|
|
635
|
+
if (identity.type !== "person") {
|
|
636
|
+
return failure("links attach only to a person identity");
|
|
637
|
+
}
|
|
638
|
+
if (identity.status !== "active") {
|
|
639
|
+
return failure("links attach only to active persons");
|
|
640
|
+
}
|
|
641
|
+
const body = await readBody(request);
|
|
642
|
+
const kind = body.kind;
|
|
643
|
+
const externalId = body.externalId;
|
|
644
|
+
if (typeof kind !== "string" || !LINK_KINDS.has(kind)) {
|
|
645
|
+
return failure(`kind must be one of: ${[...LINK_KINDS].join(", ")}`);
|
|
646
|
+
}
|
|
647
|
+
if (typeof externalId !== "string" || !externalId) {
|
|
648
|
+
return failure("externalId is required");
|
|
649
|
+
}
|
|
650
|
+
const existing = state.identityLinks.find((l) => l.kind === kind && l.externalId === externalId);
|
|
651
|
+
if (existing) {
|
|
652
|
+
if (existing.identityId === identityId) return success({ link: existing });
|
|
653
|
+
return failure("identifier already linked to another identity", 409);
|
|
654
|
+
}
|
|
655
|
+
const link = {
|
|
656
|
+
id: crypto3.randomUUID(),
|
|
657
|
+
identityId,
|
|
658
|
+
kind,
|
|
659
|
+
externalId,
|
|
660
|
+
verified: body.verified === true,
|
|
661
|
+
createdAt: now()
|
|
662
|
+
};
|
|
663
|
+
state.identityLinks.push(link);
|
|
664
|
+
return success({ link });
|
|
665
|
+
}
|
|
666
|
+
async function handleIdentitiesRequest(request, subPath) {
|
|
667
|
+
const method = request.method;
|
|
668
|
+
if (subPath === "" || subPath === "/") {
|
|
669
|
+
if (method === "GET") return handleList(request);
|
|
670
|
+
if (method === "POST") return handleCreate(request);
|
|
671
|
+
}
|
|
672
|
+
const linksMatch = subPath.match(/^\/([^/]+)\/links$/);
|
|
673
|
+
if (linksMatch && method === "POST") {
|
|
674
|
+
return handleAttachLink(linksMatch[1], request);
|
|
675
|
+
}
|
|
676
|
+
const singleMatch = subPath.match(/^\/([^/]+)$/);
|
|
677
|
+
if (singleMatch) {
|
|
678
|
+
if (method === "GET") return handleGet(singleMatch[1]);
|
|
679
|
+
if (method === "PATCH") return handleUpdate(singleMatch[1], request);
|
|
680
|
+
}
|
|
681
|
+
return failure(`No identities simulator for ${method} .../identities${subPath}`, 404);
|
|
682
|
+
}
|
|
683
|
+
|
|
548
684
|
// src/simulator/router.ts
|
|
549
685
|
var fetchHolder = globalSingleton("fetch-holder", () => ({
|
|
550
686
|
originalFetch: null
|
|
@@ -562,7 +698,8 @@ async function handleSimulatedRequest(request, url) {
|
|
|
562
698
|
}
|
|
563
699
|
const dataStoreMatch = url.pathname.match(/^\/api\/data-stores\/[^/]+(\/.*)?$/);
|
|
564
700
|
const isEmail = url.pathname === "/api/email/send";
|
|
565
|
-
|
|
701
|
+
const identitiesMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/identities(\/.*)?$/);
|
|
702
|
+
if (dataStoreMatch || isEmail || identitiesMatch) {
|
|
566
703
|
const authHeader = request.headers.get("X-Stardeck-Auth");
|
|
567
704
|
if (!authHeader) {
|
|
568
705
|
return failure("Missing authentication header", 401);
|
|
@@ -575,28 +712,31 @@ async function handleSimulatedRequest(request, url) {
|
|
|
575
712
|
if (isEmail && request.method === "POST") {
|
|
576
713
|
return handleEmailSend(request);
|
|
577
714
|
}
|
|
715
|
+
if (identitiesMatch) {
|
|
716
|
+
return handleIdentitiesRequest(request, identitiesMatch[1] ?? "");
|
|
717
|
+
}
|
|
578
718
|
if (dataStoreMatch) {
|
|
579
719
|
const subPath = dataStoreMatch[1] ?? "";
|
|
580
720
|
const db = requireDb();
|
|
581
|
-
const
|
|
721
|
+
const readBody2 = async () => await request.json();
|
|
582
722
|
if (subPath === "/query" && request.method === "POST") {
|
|
583
|
-
return handleQuery(db, await
|
|
723
|
+
return handleQuery(db, await readBody2());
|
|
584
724
|
}
|
|
585
725
|
if (subPath === "/mutate" && request.method === "POST") {
|
|
586
|
-
return handleMutate(db, await
|
|
726
|
+
return handleMutate(db, await readBody2());
|
|
587
727
|
}
|
|
588
728
|
if (subPath === "/schema" && request.method === "GET") {
|
|
589
729
|
return handleGetSchema(db);
|
|
590
730
|
}
|
|
591
731
|
if (subPath === "/schema/tables" && request.method === "POST") {
|
|
592
|
-
return handleCreateTable(db, await
|
|
732
|
+
return handleCreateTable(db, await readBody2());
|
|
593
733
|
}
|
|
594
734
|
if (subPath === "/schema/columns" && request.method === "POST") {
|
|
595
|
-
return handleAddColumn(db, await
|
|
735
|
+
return handleAddColumn(db, await readBody2());
|
|
596
736
|
}
|
|
597
737
|
}
|
|
598
738
|
return failure(
|
|
599
|
-
`[stardeck-testing] No simulator for ${request.method} ${url.pathname}. Supported: data-store query/mutate/schema, email send, auth verify/refresh, Neon /sql.`,
|
|
739
|
+
`[stardeck-testing] No simulator for ${request.method} ${url.pathname}. Supported: data-store query/mutate/schema, email send, identities CRUD, auth verify/refresh, Neon /sql.`,
|
|
600
740
|
404
|
|
601
741
|
);
|
|
602
742
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stardeck-customer-apps/testing",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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",
|
|
@@ -62,13 +62,18 @@
|
|
|
62
62
|
"author": "Stardeck",
|
|
63
63
|
"license": "MIT",
|
|
64
64
|
"dependencies": {
|
|
65
|
-
"@electric-sql/pglite": "^0.3.0"
|
|
65
|
+
"@electric-sql/pglite": "^0.3.0",
|
|
66
|
+
"@stardeck-customer-apps/core": "*"
|
|
66
67
|
},
|
|
67
68
|
"peerDependencies": {
|
|
69
|
+
"@stardeck-customer-apps/integrations-sdk": ">=1.6.0",
|
|
68
70
|
"next": "^14.0.0 || ^15.0.0 || ^16.0.0",
|
|
69
71
|
"vitest": ">=2.0.0"
|
|
70
72
|
},
|
|
71
73
|
"peerDependenciesMeta": {
|
|
74
|
+
"@stardeck-customer-apps/integrations-sdk": {
|
|
75
|
+
"optional": true
|
|
76
|
+
},
|
|
72
77
|
"next": {
|
|
73
78
|
"optional": true
|
|
74
79
|
}
|
|
@@ -78,6 +83,7 @@
|
|
|
78
83
|
"@neondatabase/serverless": "^1.0.0",
|
|
79
84
|
"@stardeck-customer-apps/data-store-sdk": "*",
|
|
80
85
|
"@stardeck-customer-apps/email-sdk": "*",
|
|
86
|
+
"@stardeck-customer-apps/integrations-sdk": "*",
|
|
81
87
|
"@stardeck-customer-apps/tsconfig": "*",
|
|
82
88
|
"@types/node": "^24.10.1",
|
|
83
89
|
"kysely": "^0.27.0",
|