@stardeck-customer-apps/testing 0.5.1 → 0.6.1
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 +57 -6
- package/dist/index.d.mts +16 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +373 -30
- package/dist/index.mjs +373 -30
- package/dist/setup.js +293 -38
- package/dist/setup.mjs +293 -38
- package/package.json +1 -1
package/dist/setup.js
CHANGED
|
@@ -87,6 +87,14 @@ var TEST_ENV_DEFAULTS = {
|
|
|
87
87
|
// src/simulator/hmac.ts
|
|
88
88
|
var import_node_crypto = __toESM(require("crypto"));
|
|
89
89
|
var TIMESTAMP_TOLERANCE_SECONDS = 300;
|
|
90
|
+
function isNonEmptyString(value) {
|
|
91
|
+
return typeof value === "string" && value.length > 0;
|
|
92
|
+
}
|
|
93
|
+
function isDeploymentAuthPayload(value) {
|
|
94
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
95
|
+
const payload = value;
|
|
96
|
+
return payload.type === "deployment-request" && isNonEmptyString(payload.organizationId) && isNonEmptyString(payload.projectId) && isNonEmptyString(payload.deploymentId) && typeof payload.timestamp === "number" && Number.isSafeInteger(payload.timestamp) && isNonEmptyString(payload.nonce);
|
|
97
|
+
}
|
|
90
98
|
function verifyDeploymentAuthHeader(secret, header) {
|
|
91
99
|
const dotIndex = header.lastIndexOf(".");
|
|
92
100
|
if (dotIndex === -1) return null;
|
|
@@ -104,16 +112,16 @@ function verifyDeploymentAuthHeader(secret, header) {
|
|
|
104
112
|
if (expectedBuf.length !== actualBuf.length || !import_node_crypto.default.timingSafeEqual(expectedBuf, actualBuf)) {
|
|
105
113
|
return null;
|
|
106
114
|
}
|
|
107
|
-
let
|
|
115
|
+
let parsed;
|
|
108
116
|
try {
|
|
109
|
-
|
|
117
|
+
parsed = JSON.parse(payloadJson);
|
|
110
118
|
} catch {
|
|
111
119
|
return null;
|
|
112
120
|
}
|
|
113
|
-
if (
|
|
121
|
+
if (!isDeploymentAuthPayload(parsed)) return null;
|
|
114
122
|
const now3 = Math.floor(Date.now() / 1e3);
|
|
115
|
-
if (Math.abs(now3 -
|
|
116
|
-
return
|
|
123
|
+
if (Math.abs(now3 - parsed.timestamp) > TIMESTAMP_TOLERANCE_SECONDS) return null;
|
|
124
|
+
return parsed;
|
|
117
125
|
}
|
|
118
126
|
|
|
119
127
|
// src/simulator/http.ts
|
|
@@ -601,25 +609,128 @@ async function handleEmailSend(request) {
|
|
|
601
609
|
|
|
602
610
|
// src/simulator/identities.ts
|
|
603
611
|
var import_node_crypto3 = __toESM(require("crypto"));
|
|
604
|
-
var
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
"
|
|
612
|
-
|
|
612
|
+
var ATTACH_LINK_KINDS = /* @__PURE__ */ new Set(["line", "facebook", "instagram", "email", "phone"]);
|
|
613
|
+
var RESOLVE_KINDS = /* @__PURE__ */ new Set(["phone", "email", "line"]);
|
|
614
|
+
var E164_PHONE = /^\+[1-9]\d{1,14}$/;
|
|
615
|
+
var SEARCH_DEFAULT_LIMIT = 50;
|
|
616
|
+
var SEARCH_MAX_LIMIT = 100;
|
|
617
|
+
function asciiLowercaseTrim(value) {
|
|
618
|
+
const trimmed = value.trim();
|
|
619
|
+
let out = "";
|
|
620
|
+
for (let i = 0; i < trimmed.length; i++) {
|
|
621
|
+
const code = trimmed.charCodeAt(i);
|
|
622
|
+
out += code >= 65 && code <= 90 ? String.fromCharCode(code + 32) : trimmed[i];
|
|
623
|
+
}
|
|
624
|
+
return out;
|
|
625
|
+
}
|
|
626
|
+
var IdentityGraphError = class extends Error {
|
|
627
|
+
};
|
|
613
628
|
function now() {
|
|
614
629
|
return (/* @__PURE__ */ new Date()).toISOString();
|
|
615
630
|
}
|
|
616
631
|
function linksFor(identityId) {
|
|
617
632
|
return state.identityLinks.filter((l) => l.identityId === identityId);
|
|
618
633
|
}
|
|
634
|
+
function comparableExternalId(kind, externalId) {
|
|
635
|
+
if (kind === "email") return asciiLowercaseTrim(externalId);
|
|
636
|
+
return externalId;
|
|
637
|
+
}
|
|
638
|
+
function normalizeExternalId(kind, externalId) {
|
|
639
|
+
if (kind === "email") {
|
|
640
|
+
const normalized = asciiLowercaseTrim(externalId);
|
|
641
|
+
return normalized.length > 0 ? { externalId: normalized } : { error: "externalId is required" };
|
|
642
|
+
}
|
|
643
|
+
if (kind === "phone" && !E164_PHONE.test(externalId)) {
|
|
644
|
+
return { error: "phone externalId must be an E.164 number" };
|
|
645
|
+
}
|
|
646
|
+
return { externalId };
|
|
647
|
+
}
|
|
648
|
+
function normalizeLinkInput(kind, externalId, allowedKinds, kindError) {
|
|
649
|
+
if (typeof kind !== "string" || !allowedKinds.has(kind)) {
|
|
650
|
+
return { error: kindError };
|
|
651
|
+
}
|
|
652
|
+
if (typeof externalId !== "string" || externalId.length === 0) {
|
|
653
|
+
return { error: "externalId is required" };
|
|
654
|
+
}
|
|
655
|
+
const normalized = normalizeExternalId(kind, externalId);
|
|
656
|
+
if ("error" in normalized) return normalized;
|
|
657
|
+
return { kind, externalId: normalized.externalId };
|
|
658
|
+
}
|
|
659
|
+
function normalizeResolveInput(kind, externalId) {
|
|
660
|
+
const normalized = normalizeLinkInput(
|
|
661
|
+
kind,
|
|
662
|
+
externalId,
|
|
663
|
+
RESOLVE_KINDS,
|
|
664
|
+
"kind must be one of: phone, email, line"
|
|
665
|
+
);
|
|
666
|
+
if ("error" in normalized) return normalized;
|
|
667
|
+
return { kind: normalized.kind, externalId: normalized.externalId };
|
|
668
|
+
}
|
|
669
|
+
function canonicalIdentity(identityId) {
|
|
670
|
+
const visited = /* @__PURE__ */ new Set();
|
|
671
|
+
let identity = state.identities.get(identityId);
|
|
672
|
+
while (identity) {
|
|
673
|
+
if (visited.has(identity.id)) {
|
|
674
|
+
throw new IdentityGraphError("identity merge chain contains a cycle");
|
|
675
|
+
}
|
|
676
|
+
visited.add(identity.id);
|
|
677
|
+
if (identity.status === "archived") {
|
|
678
|
+
throw new IdentityGraphError("identity is archived");
|
|
679
|
+
}
|
|
680
|
+
if (identity.status === "active" && !identity.mergedIntoId) {
|
|
681
|
+
return identity;
|
|
682
|
+
}
|
|
683
|
+
if (!identity.mergedIntoId) {
|
|
684
|
+
throw new IdentityGraphError("identity merge chain has no active canonical target");
|
|
685
|
+
}
|
|
686
|
+
identity = state.identities.get(identity.mergedIntoId);
|
|
687
|
+
}
|
|
688
|
+
throw new IdentityGraphError("identity merge chain references a missing identity");
|
|
689
|
+
}
|
|
690
|
+
function matchingLinks(kind, externalId) {
|
|
691
|
+
const comparable = comparableExternalId(kind, externalId);
|
|
692
|
+
return state.identityLinks.filter(
|
|
693
|
+
(link) => link.kind === kind && comparableExternalId(link.kind, link.externalId) === comparable
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
function createPerson(displayName) {
|
|
697
|
+
const createdAt = now();
|
|
698
|
+
const identity = {
|
|
699
|
+
id: import_node_crypto3.default.randomUUID(),
|
|
700
|
+
type: "person",
|
|
701
|
+
parentId: null,
|
|
702
|
+
displayName,
|
|
703
|
+
profile: {},
|
|
704
|
+
status: "active",
|
|
705
|
+
mergedIntoId: null,
|
|
706
|
+
externalRef: null,
|
|
707
|
+
createdAt,
|
|
708
|
+
updatedAt: createdAt
|
|
709
|
+
};
|
|
710
|
+
state.identities.set(identity.id, identity);
|
|
711
|
+
return identity;
|
|
712
|
+
}
|
|
713
|
+
function createUnverifiedLink(identityId, kind, externalId) {
|
|
714
|
+
const link = {
|
|
715
|
+
id: import_node_crypto3.default.randomUUID(),
|
|
716
|
+
identityId,
|
|
717
|
+
kind,
|
|
718
|
+
externalId,
|
|
719
|
+
// Deployment credentials can attest provenance, but cannot manufacture a
|
|
720
|
+
// platform-verified fact. Trusted channel/control-plane setup uses the
|
|
721
|
+
// directory's test-only seedVerifiedLink helper instead.
|
|
722
|
+
verified: false,
|
|
723
|
+
createdAt: now()
|
|
724
|
+
};
|
|
725
|
+
state.identityLinks.push(link);
|
|
726
|
+
return link;
|
|
727
|
+
}
|
|
619
728
|
function handleList(request) {
|
|
620
729
|
const typeParam = new URL(request.url).searchParams.get("type");
|
|
621
730
|
const type = typeParam === "person" || typeParam === "account" ? typeParam : void 0;
|
|
622
|
-
const identities = [...state.identities.values()]
|
|
731
|
+
const identities = [...state.identities.values()].filter(
|
|
732
|
+
(identity) => identity.status !== "merged"
|
|
733
|
+
);
|
|
623
734
|
return success({ identities: type ? identities.filter((i) => i.type === type) : identities });
|
|
624
735
|
}
|
|
625
736
|
async function handleCreate(request) {
|
|
@@ -637,6 +748,7 @@ async function handleCreate(request) {
|
|
|
637
748
|
}
|
|
638
749
|
parentId = parent.id;
|
|
639
750
|
}
|
|
751
|
+
const createdAt = now();
|
|
640
752
|
const identity = {
|
|
641
753
|
id: import_node_crypto3.default.randomUUID(),
|
|
642
754
|
type,
|
|
@@ -646,8 +758,8 @@ async function handleCreate(request) {
|
|
|
646
758
|
status: "active",
|
|
647
759
|
mergedIntoId: null,
|
|
648
760
|
externalRef: body.externalRef ?? null,
|
|
649
|
-
createdAt
|
|
650
|
-
updatedAt:
|
|
761
|
+
createdAt,
|
|
762
|
+
updatedAt: createdAt
|
|
651
763
|
};
|
|
652
764
|
state.identities.set(identity.id, identity);
|
|
653
765
|
return success({ identity });
|
|
@@ -683,36 +795,166 @@ async function handleAttachLink(identityId, request) {
|
|
|
683
795
|
return failure("links attach only to active persons");
|
|
684
796
|
}
|
|
685
797
|
const body = await readJsonBody(request);
|
|
686
|
-
const
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
}
|
|
694
|
-
const existing =
|
|
798
|
+
const normalized = normalizeLinkInput(
|
|
799
|
+
body.kind,
|
|
800
|
+
body.externalId,
|
|
801
|
+
ATTACH_LINK_KINDS,
|
|
802
|
+
`kind must be one of: ${[...ATTACH_LINK_KINDS].join(", ")}`
|
|
803
|
+
);
|
|
804
|
+
if ("error" in normalized) return failure(normalized.error);
|
|
805
|
+
const { kind, externalId } = normalized;
|
|
806
|
+
const [existing] = matchingLinks(kind, externalId);
|
|
695
807
|
if (existing) {
|
|
696
808
|
if (existing.identityId === identityId) return success({ link: existing });
|
|
697
809
|
return failure("identifier already linked to another identity", 409);
|
|
698
810
|
}
|
|
699
|
-
const link =
|
|
700
|
-
id: import_node_crypto3.default.randomUUID(),
|
|
701
|
-
identityId,
|
|
702
|
-
kind,
|
|
703
|
-
externalId,
|
|
704
|
-
verified: body.verified === true,
|
|
705
|
-
createdAt: now()
|
|
706
|
-
};
|
|
707
|
-
state.identityLinks.push(link);
|
|
811
|
+
const link = createUnverifiedLink(identityId, kind, externalId);
|
|
708
812
|
return success({ link });
|
|
709
813
|
}
|
|
814
|
+
async function handleResolve(request) {
|
|
815
|
+
const body = await readJsonBody(request);
|
|
816
|
+
const normalized = normalizeResolveInput(body.kind, body.externalId);
|
|
817
|
+
if ("error" in normalized) return failure(normalized.error);
|
|
818
|
+
const provenance = body.provenance;
|
|
819
|
+
if (provenance !== "guest" && provenance !== "staff") {
|
|
820
|
+
return failure("provenance must be 'guest' or 'staff'");
|
|
821
|
+
}
|
|
822
|
+
if (body.displayName !== void 0 && typeof body.displayName !== "string") {
|
|
823
|
+
return failure("displayName must be a string");
|
|
824
|
+
}
|
|
825
|
+
const links = matchingLinks(normalized.kind, normalized.externalId);
|
|
826
|
+
if (links.length > 0) {
|
|
827
|
+
const canonicalIds = /* @__PURE__ */ new Set();
|
|
828
|
+
for (const link of links) {
|
|
829
|
+
try {
|
|
830
|
+
canonicalIds.add(canonicalIdentity(link.identityId).id);
|
|
831
|
+
} catch (error) {
|
|
832
|
+
const message = error instanceof Error ? error.message : "invalid identity merge chain";
|
|
833
|
+
return failure(`cannot resolve identity link: ${message}`, 409);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
if (canonicalIds.size !== 1) {
|
|
837
|
+
return failure("identifier is linked to multiple identities", 409);
|
|
838
|
+
}
|
|
839
|
+
const canonicalId = [...canonicalIds][0];
|
|
840
|
+
const hasVerifiedLink = links.some((link) => link.verified);
|
|
841
|
+
if (provenance === "guest" && hasVerifiedLink) {
|
|
842
|
+
return success({ identityId: null, reason: "verified_conflict" });
|
|
843
|
+
}
|
|
844
|
+
return success({ identityId: canonicalId, created: false });
|
|
845
|
+
}
|
|
846
|
+
const displayName = body.displayName ?? null;
|
|
847
|
+
const identity = createPerson(displayName);
|
|
848
|
+
createUnverifiedLink(identity.id, normalized.kind, normalized.externalId);
|
|
849
|
+
return success({ identityId: identity.id, created: true });
|
|
850
|
+
}
|
|
851
|
+
function encodeSearchCursor(cursor) {
|
|
852
|
+
return Buffer.from(JSON.stringify(cursor), "utf8").toString("base64url");
|
|
853
|
+
}
|
|
854
|
+
function decodeSearchCursor(raw, query) {
|
|
855
|
+
try {
|
|
856
|
+
const rawParsed = JSON.parse(Buffer.from(raw, "base64url").toString("utf8"));
|
|
857
|
+
if (!rawParsed || typeof rawParsed !== "object" || Array.isArray(rawParsed)) return null;
|
|
858
|
+
const parsed = rawParsed;
|
|
859
|
+
if (typeof parsed.createdAt !== "string" || typeof parsed.id !== "string" || typeof parsed.query !== "string" || parsed.query !== query) {
|
|
860
|
+
return null;
|
|
861
|
+
}
|
|
862
|
+
return { createdAt: parsed.createdAt, id: parsed.id, query: parsed.query };
|
|
863
|
+
} catch {
|
|
864
|
+
return null;
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
function compareIdentityOrder(left, right) {
|
|
868
|
+
const byCreatedAt = left.createdAt.localeCompare(right.createdAt);
|
|
869
|
+
return byCreatedAt !== 0 ? byCreatedAt : left.id.localeCompare(right.id);
|
|
870
|
+
}
|
|
871
|
+
function identityMatchesQuery(identity, query) {
|
|
872
|
+
if (!query) return true;
|
|
873
|
+
if (identity.displayName?.toLowerCase().includes(query)) return true;
|
|
874
|
+
return state.identityLinks.some(
|
|
875
|
+
(link) => link.identityId === identity.id && comparableExternalId(link.kind, link.externalId).toLowerCase().includes(query)
|
|
876
|
+
);
|
|
877
|
+
}
|
|
878
|
+
function handleSearch(request) {
|
|
879
|
+
const url = new URL(request.url);
|
|
880
|
+
const query = (url.searchParams.get("query") ?? "").trim().toLowerCase();
|
|
881
|
+
const rawLimit = url.searchParams.get("limit");
|
|
882
|
+
let limit = SEARCH_DEFAULT_LIMIT;
|
|
883
|
+
if (rawLimit !== null) {
|
|
884
|
+
if (!/^\d+$/.test(rawLimit)) return failure("limit must be a positive integer");
|
|
885
|
+
limit = Number(rawLimit);
|
|
886
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > SEARCH_MAX_LIMIT) {
|
|
887
|
+
return failure(`limit must be between 1 and ${SEARCH_MAX_LIMIT}`);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
const rawCursor = url.searchParams.get("cursor");
|
|
891
|
+
const cursor = rawCursor ? decodeSearchCursor(rawCursor, query) : null;
|
|
892
|
+
if (rawCursor && !cursor) return failure("cursor is invalid");
|
|
893
|
+
const identities = [...state.identities.values()].filter((identity) => identity.status === "active" && identity.mergedIntoId === null).filter((identity) => identityMatchesQuery(identity, query)).sort(compareIdentityOrder);
|
|
894
|
+
const start = cursor ? identities.findIndex((identity) => compareIdentityOrder(identity, cursor) > 0) : 0;
|
|
895
|
+
const pageStart = start < 0 ? identities.length : start;
|
|
896
|
+
const items = identities.slice(pageStart, pageStart + limit);
|
|
897
|
+
const hasMore = pageStart + items.length < identities.length;
|
|
898
|
+
const nextCursor = hasMore ? encodeSearchCursor({
|
|
899
|
+
createdAt: items[items.length - 1].createdAt,
|
|
900
|
+
id: items[items.length - 1].id,
|
|
901
|
+
query
|
|
902
|
+
}) : null;
|
|
903
|
+
return success({ items, nextCursor });
|
|
904
|
+
}
|
|
905
|
+
function aliasesForCanonical(canonicalId) {
|
|
906
|
+
const aliases = [canonicalId];
|
|
907
|
+
for (const identity of state.identities.values()) {
|
|
908
|
+
if (identity.id === canonicalId || identity.status !== "merged") continue;
|
|
909
|
+
try {
|
|
910
|
+
if (canonicalIdentity(identity.id).id === canonicalId) aliases.push(identity.id);
|
|
911
|
+
} catch {
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
return aliases;
|
|
915
|
+
}
|
|
916
|
+
async function handleAliases(request) {
|
|
917
|
+
const body = await readJsonBody(request);
|
|
918
|
+
if (!Array.isArray(body.identityIds)) {
|
|
919
|
+
return failure("identityIds must be an array");
|
|
920
|
+
}
|
|
921
|
+
if (body.identityIds.length === 0) {
|
|
922
|
+
return failure("identityIds must not be empty");
|
|
923
|
+
}
|
|
924
|
+
if (body.identityIds.length > 200) {
|
|
925
|
+
return failure("A maximum of 200 identity ids may be requested");
|
|
926
|
+
}
|
|
927
|
+
if (body.identityIds.some((identityId) => typeof identityId !== "string" || identityId.length === 0)) {
|
|
928
|
+
return failure("identityIds must contain non-empty strings");
|
|
929
|
+
}
|
|
930
|
+
const aliases = {};
|
|
931
|
+
for (const identityId of body.identityIds) {
|
|
932
|
+
let canonical;
|
|
933
|
+
try {
|
|
934
|
+
canonical = canonicalIdentity(identityId);
|
|
935
|
+
} catch (error) {
|
|
936
|
+
const message = error instanceof Error ? error.message : "invalid identity merge chain";
|
|
937
|
+
return failure(`cannot expand identity aliases: ${message}`, 409);
|
|
938
|
+
}
|
|
939
|
+
aliases[canonical.id] ??= aliasesForCanonical(canonical.id);
|
|
940
|
+
}
|
|
941
|
+
return success({ aliases });
|
|
942
|
+
}
|
|
710
943
|
async function handleIdentitiesRequest(request, subPath) {
|
|
711
944
|
const method = request.method;
|
|
712
945
|
if (subPath === "" || subPath === "/") {
|
|
713
946
|
if (method === "GET") return handleList(request);
|
|
714
947
|
if (method === "POST") return handleCreate(request);
|
|
715
948
|
}
|
|
949
|
+
if (subPath === "/resolve" && method === "POST") {
|
|
950
|
+
return handleResolve(request);
|
|
951
|
+
}
|
|
952
|
+
if (subPath === "/aliases" && method === "POST") {
|
|
953
|
+
return handleAliases(request);
|
|
954
|
+
}
|
|
955
|
+
if (subPath === "/search" && method === "GET") {
|
|
956
|
+
return handleSearch(request);
|
|
957
|
+
}
|
|
716
958
|
const linksMatch = subPath.match(/^\/([^/]+)\/links$/);
|
|
717
959
|
if (linksMatch && method === "POST") {
|
|
718
960
|
return handleAttachLink(linksMatch[1], request);
|
|
@@ -1361,13 +1603,24 @@ function requiresDeploymentHmac(request, url) {
|
|
|
1361
1603
|
const edgeMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/edge(\/.*)?$/);
|
|
1362
1604
|
return !!(dataStoreMatch || isEmail || identitiesMatch || storeMatch || messagingMatch || edgeMatch);
|
|
1363
1605
|
}
|
|
1606
|
+
function deploymentIdFromPath(url) {
|
|
1607
|
+
const match = url.pathname.match(/\/api\/deployments\/([^/]+)(?:\/|$)/);
|
|
1608
|
+
if (!match) return null;
|
|
1609
|
+
try {
|
|
1610
|
+
return decodeURIComponent(match[1]);
|
|
1611
|
+
} catch {
|
|
1612
|
+
return match[1];
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1364
1615
|
async function handleSimulatedRequest(request, url) {
|
|
1365
1616
|
if (url.pathname === "/sql") {
|
|
1366
1617
|
return handleNeonSql(requireDb(), request);
|
|
1367
1618
|
}
|
|
1368
|
-
const authMatch = url.pathname.match(
|
|
1619
|
+
const authMatch = url.pathname.match(
|
|
1620
|
+
/^\/api\/deployments\/[^/]+\/auth\/(v2\/verify|verify|refresh)$/
|
|
1621
|
+
);
|
|
1369
1622
|
if (authMatch) {
|
|
1370
|
-
return authMatch[1]
|
|
1623
|
+
return authMatch[1].endsWith("verify") ? handleAuthVerify(request) : handleAuthRefresh(request);
|
|
1371
1624
|
}
|
|
1372
1625
|
const dataStoreMatch = url.pathname.match(/^\/api\/data-stores\/[^/]+(\/.*)?$/);
|
|
1373
1626
|
const isEmail = url.pathname === "/api/email/send";
|
|
@@ -1384,7 +1637,9 @@ async function handleSimulatedRequest(request, url) {
|
|
|
1384
1637
|
return failure("Missing authentication header", 401);
|
|
1385
1638
|
}
|
|
1386
1639
|
const secret = process.env.DEPLOYMENT_SECRET;
|
|
1387
|
-
|
|
1640
|
+
const authPayload = secret ? verifyDeploymentAuthHeader(secret, authHeader) : null;
|
|
1641
|
+
const pathDeploymentId = deploymentIdFromPath(url);
|
|
1642
|
+
if (!authPayload || pathDeploymentId !== null && authPayload.deploymentId !== pathDeploymentId) {
|
|
1388
1643
|
return failure("Invalid authentication", 401);
|
|
1389
1644
|
}
|
|
1390
1645
|
}
|