@tokenoftrust/storefront-runner 4.8.0 → 4.9.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/apps/storefront/astro.config.mjs +2 -1
- package/apps/storefront/dev-plugins/dev-auth-outbox.mjs +181 -0
- package/apps/storefront/src/lib/access/auth/devAuth.ts +34 -2
- package/apps/storefront/src/lib/access/auth/route.ts +36 -2
- package/apps/storefront/src/lib/access/auth/totAccessClient.ts +6 -18
- package/apps/storefront/src/lib/access/dashboard/staffStoreEntry.ts +2 -1
- package/apps/storefront/src/lib/access/dashboard/staffVendorAccess.ts +6 -10
- package/apps/storefront/src/lib/access/shared/accessExplain.ts +106 -28
- package/apps/storefront/src/lib/access/shared/developerInvites.ts +40 -38
- package/apps/storefront/src/lib/access/shared/invitePreflight.ts +1 -1
- package/apps/storefront/src/lib/access/shared/principalGrants.ts +16 -17
- package/apps/storefront/src/lib/engagement/messaging/messagesClient.ts +13 -7
- package/apps/storefront/src/lib/observability/diag/failureReason.ts +37 -0
- package/apps/storefront/src/lib/observability/diag/redact.ts +11 -14
- package/apps/storefront/src/pages/api/auth/magic-exchange.ts +4 -7
- package/apps/storefront/src/pages/api/auth/send.ts +12 -12
- package/apps/storefront/src/pages/api/auth/step-up-verify.ts +2 -1
- package/apps/storefront/src/pages/api/auth/verify.ts +7 -3
- package/apps/storefront/src/pages/auth/login.astro +24 -6
- package/apps/storefront/src/pages/auth/magic.astro +4 -7
- package/package.json +1 -1
- package/packages/public-runtime/src/image-dependencies.ts +16 -5
- package/packages/public-runtime/src/image-release-gate.ts +62 -4
|
@@ -83,6 +83,12 @@ export interface ImageDependency {
|
|
|
83
83
|
surface: ImageReferenceSurface;
|
|
84
84
|
disposition: ImageDependencyDisposition;
|
|
85
85
|
reason?: ImageDependencyReason;
|
|
86
|
+
/**
|
|
87
|
+
* Set when the reference is an absolute URL on one of the tenant's own origins: the path of the
|
|
88
|
+
* artifact that holds it. Such a URL is served by the tenant's origin or by nothing, so a gate can
|
|
89
|
+
* name the file and ask whether a repo file backs it (`tenant-origin-images.ts`).
|
|
90
|
+
*/
|
|
91
|
+
tenantOriginIn?: string;
|
|
86
92
|
}
|
|
87
93
|
|
|
88
94
|
export interface ImageDependencyFinding {
|
|
@@ -1214,11 +1220,15 @@ export function classifyImageDependency(
|
|
|
1214
1220
|
tenantOrigins?: readonly string[];
|
|
1215
1221
|
},
|
|
1216
1222
|
): Pick<ImageDependency, "canonicalLocator" | "disposition" | "reason"> {
|
|
1217
|
-
|
|
1223
|
+
const { tenantOrigin: _tenantOrigin, ...classified } = classifyImageDependencyWithPrepared(
|
|
1218
1224
|
locator, context, preparePlatformHostedConfig(context.platformHosted), prepareTenantOrigins(context.tenantOrigins ?? []),
|
|
1219
1225
|
);
|
|
1226
|
+
return classified;
|
|
1220
1227
|
}
|
|
1221
1228
|
|
|
1229
|
+
/** A classification, and whether it came from an absolute URL on one of the tenant's own origins. */
|
|
1230
|
+
type Classified = Pick<ImageDependency, "canonicalLocator" | "disposition" | "reason"> & { tenantOrigin?: true };
|
|
1231
|
+
|
|
1222
1232
|
function prepareTenantOrigins(tenantOrigins: readonly string[]): ReadonlySet<string> {
|
|
1223
1233
|
return new Set(tenantOrigins.map(normalizedOrigin).filter((origin): origin is string => origin != null));
|
|
1224
1234
|
}
|
|
@@ -1228,7 +1238,7 @@ function classifyParsedHttpUrl(
|
|
|
1228
1238
|
context: { tenantId: string; artifactPath: string },
|
|
1229
1239
|
platformHosted: PreparedPlatformHostedImageConfig,
|
|
1230
1240
|
tenantOrigins: ReadonlySet<string>,
|
|
1231
|
-
):
|
|
1241
|
+
): Classified {
|
|
1232
1242
|
if (!absolute.hostname || absolute.username || absolute.password) {
|
|
1233
1243
|
return { canonicalLocator: "invalid:", disposition: "security-error", reason: "malformed" };
|
|
1234
1244
|
}
|
|
@@ -1240,7 +1250,7 @@ function classifyParsedHttpUrl(
|
|
|
1240
1250
|
// instead of the third-party fallback below. The pathname.startsWith("//") exclusion
|
|
1241
1251
|
// guards a URL like https://tenant.example//evil.example/x.png, whose pathname would
|
|
1242
1252
|
// otherwise re-parse as scheme-relative to a DIFFERENT origin on the recursive call.
|
|
1243
|
-
return classifyImageDependencyWithPrepared(`${absolute.pathname}${absolute.search}`, context, platformHosted, tenantOrigins);
|
|
1253
|
+
return { ...classifyImageDependencyWithPrepared(`${absolute.pathname}${absolute.search}`, context, platformHosted, tenantOrigins), tenantOrigin: true };
|
|
1244
1254
|
}
|
|
1245
1255
|
absolute.hash = "";
|
|
1246
1256
|
const canonical = absolute.toString();
|
|
@@ -1259,7 +1269,7 @@ function classifyImageDependencyWithPrepared(
|
|
|
1259
1269
|
context: { tenantId: string; artifactPath: string },
|
|
1260
1270
|
platformHosted: PreparedPlatformHostedImageConfig,
|
|
1261
1271
|
tenantOrigins: ReadonlySet<string>,
|
|
1262
|
-
):
|
|
1272
|
+
): Classified {
|
|
1263
1273
|
const raw = normalizeUrlInput(locator);
|
|
1264
1274
|
if (!raw || CONTROL_RE.test(raw)) {
|
|
1265
1275
|
return { canonicalLocator: "invalid:", disposition: "security-error", reason: "malformed" };
|
|
@@ -1485,7 +1495,7 @@ export function scanImageDependencies(
|
|
|
1485
1495
|
});
|
|
1486
1496
|
continue;
|
|
1487
1497
|
}
|
|
1488
|
-
const classified = classifyImageDependencyWithPrepared(ref.classificationLocator ?? ref.locator, {
|
|
1498
|
+
const { tenantOrigin, ...classified } = classifyImageDependencyWithPrepared(ref.classificationLocator ?? ref.locator, {
|
|
1489
1499
|
tenantId: options.tenantId,
|
|
1490
1500
|
artifactPath: artifact.path,
|
|
1491
1501
|
}, platformHosted, tenantOrigins);
|
|
@@ -1495,6 +1505,7 @@ export function scanImageDependencies(
|
|
|
1495
1505
|
occurrence,
|
|
1496
1506
|
surface: ref.surface,
|
|
1497
1507
|
...classified,
|
|
1508
|
+
...(tenantOrigin ? { tenantOriginIn: artifact.path } : {}),
|
|
1498
1509
|
});
|
|
1499
1510
|
}
|
|
1500
1511
|
}
|
|
@@ -31,15 +31,45 @@ import type { ImageDependency, ImageDependencyArtifact } from "./image-dependenc
|
|
|
31
31
|
/**
|
|
32
32
|
* `unregistered` and `unsupported-format` are integrity: nothing may publish through them.
|
|
33
33
|
* `unresolved` and `mutable` judge the tenant's own references: an owner may accept them exactly.
|
|
34
|
+
* `tenant-origin-missing` is not acceptable: an absolute URL on the tenant's own domain that no
|
|
35
|
+
* file in the release backs is served by nothing once that domain points at the platform, and
|
|
36
|
+
* capture has no source to fetch it from (see {@link tenantOriginImageGaps}).
|
|
34
37
|
*/
|
|
35
|
-
export type ReleaseImageFindingCode = "unregistered" | "unsupported-format" | "unresolved" | "mutable";
|
|
38
|
+
export type ReleaseImageFindingCode = "unregistered" | "unsupported-format" | "unresolved" | "mutable" | "tenant-origin-missing";
|
|
36
39
|
|
|
37
|
-
export const BLOCKING_RELEASE_IMAGE_CODES: readonly ReleaseImageFindingCode[] = Object.freeze(["unregistered", "unsupported-format"]);
|
|
40
|
+
export const BLOCKING_RELEASE_IMAGE_CODES: readonly ReleaseImageFindingCode[] = Object.freeze(["unregistered", "unsupported-format", "tenant-origin-missing"]);
|
|
38
41
|
|
|
39
42
|
export interface ReleaseImageFinding {
|
|
40
43
|
code: ReleaseImageFindingCode;
|
|
41
44
|
/** The reference as rendered, query and fragment stripped. */
|
|
42
45
|
ref: string;
|
|
46
|
+
/** `tenant-origin-missing` only: the files that hold the reference, sorted. */
|
|
47
|
+
files?: string[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The tenant-origin image references no file backs. A reference is an absolute URL on one of the
|
|
52
|
+
* tenant's own origins (`ImageDependency.tenantOriginIn`) that scanning classified as a repo-local
|
|
53
|
+
* path to capture; `backed` says whether a file at that path exists where the caller looks (the
|
|
54
|
+
* revision's repo files before capture, the release's files at publish). One entry per reference,
|
|
55
|
+
* naming the files that hold it.
|
|
56
|
+
*/
|
|
57
|
+
export function tenantOriginImageGaps(
|
|
58
|
+
dependencies: readonly ImageDependency[],
|
|
59
|
+
backed: (path: string) => boolean,
|
|
60
|
+
): Array<{ ref: string; files: string[] }> {
|
|
61
|
+
const holders = new Map<string, Set<string>>();
|
|
62
|
+
for (const dependency of dependencies) {
|
|
63
|
+
if (dependency.tenantOriginIn === undefined || dependency.disposition !== "capture-required" || dependency.reason !== "local") continue;
|
|
64
|
+
const ref = refOf(dependency);
|
|
65
|
+
if (!ref.startsWith("/") || ref.startsWith("//")) continue;
|
|
66
|
+
if (backed(ref)) continue;
|
|
67
|
+
if (!holders.has(ref)) holders.set(ref, new Set());
|
|
68
|
+
holders.get(ref)!.add(dependency.tenantOriginIn);
|
|
69
|
+
}
|
|
70
|
+
return [...holders.entries()]
|
|
71
|
+
.map(([ref, files]) => ({ ref, files: [...files].sort() }))
|
|
72
|
+
.sort((a, b) => (a.ref < b.ref ? -1 : a.ref > b.ref ? 1 : 0));
|
|
43
73
|
}
|
|
44
74
|
|
|
45
75
|
export type BundlePathResolution =
|
|
@@ -71,7 +101,7 @@ export interface ReleaseImageFacts {
|
|
|
71
101
|
export interface LegacyImageAllowance {
|
|
72
102
|
tenant: string;
|
|
73
103
|
ref: string;
|
|
74
|
-
code: Exclude<ReleaseImageFindingCode, "unsupported-format">;
|
|
104
|
+
code: Exclude<ReleaseImageFindingCode, "unsupported-format" | "tenant-origin-missing">;
|
|
75
105
|
/** Who answers for this entry and removes it by its exit date. */
|
|
76
106
|
owner: string;
|
|
77
107
|
reason: string;
|
|
@@ -171,8 +201,16 @@ export function judgeReleaseImages(input: {
|
|
|
171
201
|
...input.dependencies.map((dependency) => ({ dependency, structured: false })),
|
|
172
202
|
...(input.structuredDependencies ?? []).map((dependency) => ({ dependency, structured: true })),
|
|
173
203
|
];
|
|
204
|
+
// A tenant-origin reference no release file backs is its own finding, whichever surface renders it.
|
|
205
|
+
const gaps = tenantOriginImageGaps(tagged.map(({ dependency }) => dependency), (path) => {
|
|
206
|
+
const rel = bundleRelPath(path);
|
|
207
|
+
return rel !== null && input.facts.bundlePath(rel).kind !== "absent";
|
|
208
|
+
});
|
|
209
|
+
const gapRefs = new Set(gaps.map((gap) => gap.ref));
|
|
210
|
+
for (const gap of gaps) raw.set(`tenant-origin-missing\0${gap.ref}`, { code: "tenant-origin-missing", ref: gap.ref, files: gap.files });
|
|
174
211
|
for (const { dependency, structured } of tagged) {
|
|
175
212
|
const ref = refOf(dependency);
|
|
213
|
+
if (dependency.tenantOriginIn !== undefined && gapRefs.has(ref)) continue;
|
|
176
214
|
const key = `${structured}\0${dependency.disposition}\0${dependency.reason ?? ""}\0${ref}`;
|
|
177
215
|
if (seen.has(key)) continue;
|
|
178
216
|
seen.add(key);
|
|
@@ -195,7 +233,7 @@ export function judgeReleaseImages(input: {
|
|
|
195
233
|
const allowed: ReleaseImageFinding[] = [];
|
|
196
234
|
const expired: LegacyImageAllowance[] = [];
|
|
197
235
|
for (const finding of [...raw.values()].sort(compareFindings)) {
|
|
198
|
-
const allowance = finding.code === "unsupported-format" ? undefined : allowanceFor(input.allowances ?? [], input.tenantId, finding);
|
|
236
|
+
const allowance = finding.code === "unsupported-format" || finding.code === "tenant-origin-missing" ? undefined : allowanceFor(input.allowances ?? [], input.tenantId, finding);
|
|
199
237
|
if (allowance && Date.parse(allowance.exit) > now) allowed.push(finding);
|
|
200
238
|
else {
|
|
201
239
|
if (allowance) expired.push(allowance);
|
|
@@ -215,6 +253,7 @@ export const RELEASE_IMAGE_FINDING_PREFIX: Readonly<Record<ReleaseImageFindingCo
|
|
|
215
253
|
"unsupported-format": "IMAGE_FORMAT",
|
|
216
254
|
unresolved: "IMAGE_UNRESOLVED",
|
|
217
255
|
mutable: "IMAGE_MUTABLE",
|
|
256
|
+
"tenant-origin-missing": "IMAGE_TENANT_ORIGIN",
|
|
218
257
|
});
|
|
219
258
|
|
|
220
259
|
/**
|
|
@@ -237,9 +276,28 @@ export function releaseImageFindingText(finding: ReleaseImageFinding): string {
|
|
|
237
276
|
case "mutable":
|
|
238
277
|
return `${prefix}: ${finding.ref} is a release-local path whose bytes are not pinned to a pool version, so the image ` +
|
|
239
278
|
"it serves can change under the same name — add it with `tot image add <file>` and reference the URL it prints.";
|
|
279
|
+
case "tenant-origin-missing":
|
|
280
|
+
return tenantOriginImageText(finding.ref, finding.files ?? [], prefix);
|
|
240
281
|
}
|
|
241
282
|
}
|
|
242
283
|
|
|
284
|
+
const NAMED_FILES = 5;
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* The text for a tenant-origin image no file backs: where it is referenced, what path it names, why
|
|
288
|
+
* that breaks, and the two remedies. Shared by the publish gates and the preview capture pass, so the
|
|
289
|
+
* remedy reads the same wherever the owner meets it.
|
|
290
|
+
*/
|
|
291
|
+
export function tenantOriginImageText(ref: string, files: readonly string[], prefix = "IMAGE_TENANT_ORIGIN"): string {
|
|
292
|
+
const shown = files.slice(0, NAMED_FILES).join(", ");
|
|
293
|
+
const more = files.length > NAMED_FILES ? ` and ${files.length - NAMED_FILES} more` : "";
|
|
294
|
+
return `${prefix}: ${ref} is referenced as a URL on this store's own domain${shown ? ` in ${shown}${more}` : ""}, but no file in the ` +
|
|
295
|
+
"repository serves that path, so the image breaks when the domain points at the platform and capture has no source to fetch it from — " +
|
|
296
|
+
"reference the image at a source URL that is not this store's domain (a Shopify store: " +
|
|
297
|
+
"`node agency-kit/tools/normalize-shopify-image-urls.mjs --store <checkout>` rewrites /cdn/shop/ references to the shop CDN URL), " +
|
|
298
|
+
"or add the image with `tot image add <file>` and reference the URL it prints.";
|
|
299
|
+
}
|
|
300
|
+
|
|
243
301
|
const ALLOWANCE_CODES = new Set(["unregistered", "unresolved", "mutable"]);
|
|
244
302
|
const DATE = /^\d{4}-\d{2}-\d{2}$/;
|
|
245
303
|
|