@spacefast/common 0.0.12 → 0.0.13
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/contracts/access.d.ts +386 -1201
- package/dist/contracts/access.js +240 -1057
- package/dist/contracts/activity.d.ts +1 -1
- package/dist/contracts/activity.js +6 -14
- package/dist/contracts/api-keys.d.ts +16 -0
- package/dist/contracts/api-keys.js +2 -0
- package/dist/contracts/archives.d.ts +1 -2
- package/dist/contracts/archives.js +4 -6
- package/dist/contracts/comments.d.ts +58 -85
- package/dist/contracts/comments.js +86 -67
- package/dist/contracts/countries.js +2 -7
- package/dist/contracts/data-source-routes.d.ts +20 -0
- package/dist/contracts/data-source-routes.js +15 -0
- package/dist/contracts/device-auth.d.ts +7 -3
- package/dist/contracts/enums.d.ts +2 -4
- package/dist/contracts/enums.js +3 -8
- package/dist/contracts/error-code-meta.d.ts +17 -20
- package/dist/contracts/error-code-meta.js +8 -5
- package/dist/contracts/error-codes.d.ts +1 -1
- package/dist/contracts/error-codes.js +4 -5
- package/dist/contracts/events.d.ts +13 -13
- package/dist/contracts/execution.d.ts +212 -0
- package/dist/contracts/execution.js +288 -0
- package/dist/contracts/feature-lifecycle.d.ts +21 -167
- package/dist/contracts/feature-lifecycle.js +27 -173
- package/dist/contracts/features.d.ts +13 -115
- package/dist/contracts/features.js +1 -31
- package/dist/contracts/functions.d.ts +729 -0
- package/dist/contracts/functions.js +485 -0
- package/dist/contracts/git.d.ts +1 -1
- package/dist/contracts/grants.d.ts +1158 -0
- package/dist/contracts/grants.js +696 -0
- package/dist/contracts/ids.d.ts +5 -3
- package/dist/contracts/ids.js +5 -3
- package/dist/contracts/mcp.d.ts +6 -6
- package/dist/contracts/oauth-resources.d.ts +5 -5
- package/dist/contracts/oauth-resources.js +1 -0
- package/dist/contracts/operations.d.ts +2 -2
- package/dist/contracts/pages.d.ts +6 -14
- package/dist/contracts/pages.js +6 -23
- package/dist/contracts/plan-policy.d.ts +1 -8
- package/dist/contracts/plan-policy.js +1 -4
- package/dist/contracts/platform.d.ts +78 -30
- package/dist/contracts/platform.js +59 -12
- package/dist/contracts/push-new.d.ts +26 -2
- package/dist/contracts/push-new.js +8 -0
- package/dist/contracts/quotas.d.ts +0 -1
- package/dist/contracts/quotas.js +0 -5
- package/dist/contracts/repository-connections.d.ts +1 -1
- package/dist/contracts/resources.d.ts +10 -66
- package/dist/contracts/resources.js +4 -10
- package/dist/contracts/runtime-api.d.ts +558 -265
- package/dist/contracts/runtime-api.js +178 -91
- package/dist/contracts/runtime-app.d.ts +138 -0
- package/dist/contracts/runtime-app.js +21 -0
- package/dist/contracts/sf-config-v1.d.ts +5 -0
- package/dist/contracts/sites.d.ts +5 -0
- package/dist/contracts/sites.js +12 -1
- package/dist/contracts/space-config.d.ts +46 -116
- package/dist/contracts/space-config.js +320 -24
- package/dist/contracts/spaces.d.ts +253 -158
- package/dist/contracts/spaces.js +68 -54
- package/dist/contracts/superadmin-emails.d.ts +2 -2
- package/dist/contracts/superadmin-spaces.d.ts +15 -130
- package/dist/contracts/superadmin-spaces.js +7 -0
- package/dist/contracts/tags.d.ts +23 -23
- package/dist/contracts/teams.d.ts +9 -9
- package/dist/contracts/transfers.d.ts +0 -1
- package/dist/contracts/zero.d.ts +195 -273
- package/dist/contracts/zero.js +78 -206
- package/dist/dashboard-paths/index.d.ts +2 -2
- package/dist/dashboard-paths/index.js +1 -0
- package/dist/docs/agent-prose.js +10 -8
- package/dist/docs/agent-setup.d.ts +2 -2
- package/dist/docs/agent-setup.js +11 -9
- package/dist/docs/agent-solutions.js +1 -1
- package/dist/docs/catalog.d.ts +28 -28
- package/dist/docs/catalog.js +29 -29
- package/dist/docs/error-docs.js +23 -27
- package/dist/utils/asset-fingerprint.d.ts +11 -0
- package/dist/utils/asset-fingerprint.js +46 -0
- package/dist/utils/auth-redirect.d.ts +1 -3
- package/dist/utils/auth-redirect.js +1 -21
- package/dist/utils/build-settings.d.ts +2 -0
- package/dist/utils/build-settings.js +61 -9
- package/dist/utils/credential-policy.d.ts +1 -1
- package/dist/utils/credential-policy.js +4 -0
- package/dist/utils/egress-policy.fixtures.json +1 -1
- package/dist/utils/grants.d.ts +53 -0
- package/dist/utils/grants.js +160 -0
- package/dist/utils/local-space-state.js +1 -0
- package/dist/utils/pages.d.ts +11 -5
- package/dist/utils/pages.js +19 -32
- package/dist/utils/publish-policy.d.ts +34 -4
- package/dist/utils/publish-policy.js +132 -61
- package/dist/utils/query-keys.d.ts +1 -6
- package/dist/utils/query-keys.js +1 -6
- package/dist/utils/space-config.d.ts +5 -1
- package/dist/utils/space-config.js +35 -0
- package/dist/utils/static-runtime-policy.d.ts +5 -0
- package/dist/utils/static-runtime-policy.js +54 -10
- package/dist/utils/upload-session.d.ts +3 -0
- package/dist/utils/upload-session.js +31 -0
- package/package.json +2 -1
- package/dist/utils/access-grant.d.ts +0 -14
- package/dist/utils/access-grant.js +0 -88
- package/dist/utils/access-match.d.ts +0 -46
- package/dist/utils/access-match.fixtures.json +0 -513
- package/dist/utils/access-match.js +0 -312
- package/dist/utils/access-verdict.fixtures.json +0 -421
- package/dist/utils/sf-config-v1.d.ts +0 -2
- package/dist/utils/sf-config-v1.fixtures.json +0 -9
- package/dist/utils/sf-config-v1.js +0 -551
|
@@ -60,6 +60,10 @@ const STATIC_EXECUTION_CONTROL_FILES = new Set([".htaccess", ".user.ini"]);
|
|
|
60
60
|
// (`/__stattic_probe` today, more `/__stattic/*` routes planned) stays
|
|
61
61
|
// reserved the same way. Mid-segment occurrences are ordinary content.
|
|
62
62
|
const STATIC_RUNTIME_CONTROL_SEGMENT_PREFIXES = ["__spacefast", "__stattic"];
|
|
63
|
+
// Compact platform endpoints live below `/__sf`. Reserve the complete root
|
|
64
|
+
// segment prefix before the first endpoint ships so uploaded content can never
|
|
65
|
+
// shadow a later platform route. Nested `__sf*` directories remain content.
|
|
66
|
+
const STATIC_RUNTIME_ROOT_SEGMENT_PREFIXES = ["__sf"];
|
|
63
67
|
// Both the current and the legacy control-path spellings stay denied: spaces
|
|
64
68
|
// published before the `.stattic` → `.spacefast` rename still serve from the
|
|
65
69
|
// legacy on-disk layout, and user archives may carry either name.
|
|
@@ -76,6 +80,24 @@ const STATIC_RUNTIME_CONTROL_PREFIXES = [
|
|
|
76
80
|
export function normalizePublishPath(relativePath) {
|
|
77
81
|
return relativePath.replace(/\\/g, "/").replace(/^\/+/g, "");
|
|
78
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* The one subtree inside the runtime control prefix that a publish may write.
|
|
85
|
+
*
|
|
86
|
+
* `sf publish` compiles a version's worker into `__spacefast/functions/bundles/`
|
|
87
|
+
* so the bundle travels through the ordinary manifest and upload path instead of
|
|
88
|
+
* needing a parallel one. Keeping it under the control prefix is what makes it
|
|
89
|
+
* unservable as static content by default — the runtime answers it only behind a
|
|
90
|
+
* signature.
|
|
91
|
+
*
|
|
92
|
+
* Scoped to `bundles/` rather than the whole `__spacefast/functions/` directory
|
|
93
|
+
* on purpose: the sibling `config.json` holds the key those signatures are
|
|
94
|
+
* minted with, and a publish that could overwrite it would be a publish that
|
|
95
|
+
* could mint its own tokens.
|
|
96
|
+
*/
|
|
97
|
+
const PUBLISHABLE_BUILD_ARTIFACT_PREFIX = "__spacefast/functions/bundles/";
|
|
98
|
+
function isPublishableBuildArtifactPath(lowerPath) {
|
|
99
|
+
return lowerPath.startsWith(PUBLISHABLE_BUILD_ARTIFACT_PREFIX);
|
|
100
|
+
}
|
|
79
101
|
export function publishPathIgnored(relativePath) {
|
|
80
102
|
const path = normalizePublishPath(relativePath);
|
|
81
103
|
// The agent state dirs hold one secret (state.json) plus committable tooling
|
|
@@ -101,27 +123,27 @@ export function publishPathIsConvention(relativePath) {
|
|
|
101
123
|
// Headers", migration diagnostics): Cloudflare Pages `_worker.js`/`_routes.json`
|
|
102
124
|
// and Pages/Netlify function directories publish as inert content with a
|
|
103
125
|
// warning so migrations are never silent.
|
|
126
|
+
function providerRuntimeFeatureForPath(path) {
|
|
127
|
+
if (path === "_worker.js") {
|
|
128
|
+
return "Cloudflare Pages _worker.js";
|
|
129
|
+
}
|
|
130
|
+
if (path === "_routes.json") {
|
|
131
|
+
return "Cloudflare Pages _routes.json";
|
|
132
|
+
}
|
|
133
|
+
if (path.startsWith("functions/")) {
|
|
134
|
+
return "Pages Functions (functions/)";
|
|
135
|
+
}
|
|
136
|
+
if (path.startsWith("netlify/functions/")) {
|
|
137
|
+
return "Netlify Functions (netlify/functions/)";
|
|
138
|
+
}
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
104
141
|
export function providerRuntimeFeatureWarnings(paths) {
|
|
105
142
|
const warnings = [];
|
|
106
143
|
const seen = new Set();
|
|
107
|
-
const featureForPath = (path) => {
|
|
108
|
-
if (path === "_worker.js") {
|
|
109
|
-
return "Cloudflare Pages _worker.js";
|
|
110
|
-
}
|
|
111
|
-
if (path === "_routes.json") {
|
|
112
|
-
return "Cloudflare Pages _routes.json";
|
|
113
|
-
}
|
|
114
|
-
if (path.startsWith("functions/")) {
|
|
115
|
-
return "Pages Functions (functions/)";
|
|
116
|
-
}
|
|
117
|
-
if (path.startsWith("netlify/functions/")) {
|
|
118
|
-
return "Netlify Functions (netlify/functions/)";
|
|
119
|
-
}
|
|
120
|
-
return null;
|
|
121
|
-
};
|
|
122
144
|
for (const rawPath of paths) {
|
|
123
145
|
const path = normalizePublishPath(rawPath);
|
|
124
|
-
const feature =
|
|
146
|
+
const feature = providerRuntimeFeatureForPath(path);
|
|
125
147
|
if (!feature || seen.has(feature)) {
|
|
126
148
|
continue;
|
|
127
149
|
}
|
|
@@ -179,11 +201,15 @@ export function staticUploadPathViolation(path, options = {}) {
|
|
|
179
201
|
if (reservedRuntimeControlFiles?.has(lowerName) && lowerPath === lowerName) {
|
|
180
202
|
return pathViolation("static_runtime_control_path_not_supported", "Static deploys cannot upload runtime control paths.", normalizedPath);
|
|
181
203
|
}
|
|
204
|
+
const lowerRootSegment = segments[0]?.toLowerCase() ?? "";
|
|
205
|
+
if (STATIC_RUNTIME_ROOT_SEGMENT_PREFIXES.some((prefix) => lowerRootSegment.startsWith(prefix))) {
|
|
206
|
+
return pathViolation("static_runtime_control_path_not_supported", "Static deploys cannot upload runtime control paths.", normalizedPath);
|
|
207
|
+
}
|
|
182
208
|
const controlSegment = segments.find((segment) => {
|
|
183
209
|
const lowerSegment = segment.toLowerCase();
|
|
184
210
|
return STATIC_RUNTIME_CONTROL_SEGMENT_PREFIXES.some((prefix) => lowerSegment.startsWith(prefix));
|
|
185
211
|
});
|
|
186
|
-
if (controlSegment) {
|
|
212
|
+
if (controlSegment && !isPublishableBuildArtifactPath(lowerPath)) {
|
|
187
213
|
return pathViolation("static_runtime_control_path_not_supported", "Static deploys cannot upload runtime control paths.", normalizedPath);
|
|
188
214
|
}
|
|
189
215
|
if (STATIC_RUNTIME_CONTROL_PREFIXES.some((prefix) => lowerPath === prefix || lowerPath.startsWith(`${prefix}/`))) {
|
|
@@ -203,6 +229,95 @@ export function firstStaticUploadPathViolation(paths, options = {}) {
|
|
|
203
229
|
}
|
|
204
230
|
return null;
|
|
205
231
|
}
|
|
232
|
+
/**
|
|
233
|
+
* Canonicalizes a route expanded from an already-canonical manifest path.
|
|
234
|
+
* Manifest intake has decoded percent escapes exactly once, so this must not
|
|
235
|
+
* decode again. It applies only the route-identity aliases shared with access.
|
|
236
|
+
*/
|
|
237
|
+
export function canonicalizePublishRequestedRoute(route) {
|
|
238
|
+
if (!route.startsWith("/")) {
|
|
239
|
+
throw new TypeError("publish requested routes must be absolute");
|
|
240
|
+
}
|
|
241
|
+
const segments = route
|
|
242
|
+
.split("/")
|
|
243
|
+
.filter((segment) => segment !== "" && segment !== ".")
|
|
244
|
+
.map((segment) => segment.normalize("NFC"));
|
|
245
|
+
if (segments.at(-1) === "index.html")
|
|
246
|
+
segments.pop();
|
|
247
|
+
return segments.length === 0 ? "/" : `/${segments.join("/")}`;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Expands artifact paths into the request routes they can serve. The runtime
|
|
251
|
+
* resolves literal files, directory indexes, and clean `.html` aliases; two
|
|
252
|
+
* different artifacts owning one request route is rejected at publish/finalize
|
|
253
|
+
* instead of depending on lookup order. `routeOwnership` exposes the same
|
|
254
|
+
* expansion in canonical access-route form so authorization and diagnostics
|
|
255
|
+
* never have to infer a route from an artifact path.
|
|
256
|
+
*/
|
|
257
|
+
export function analyzePublishRoutes(paths, options = {}) {
|
|
258
|
+
const index = options.index === undefined ? "index.html" : options.index;
|
|
259
|
+
const cleanUrls = options.cleanUrls ?? true;
|
|
260
|
+
const routes = new Map();
|
|
261
|
+
const add = (route, path) => {
|
|
262
|
+
const owners = routes.get(route) ?? new Set();
|
|
263
|
+
owners.add(path);
|
|
264
|
+
routes.set(route, owners);
|
|
265
|
+
};
|
|
266
|
+
for (const path of paths) {
|
|
267
|
+
add(`/${path}`, path);
|
|
268
|
+
if (cleanUrls && path.endsWith(".html")) {
|
|
269
|
+
add(`/${path.slice(0, -".html".length)}`, path);
|
|
270
|
+
}
|
|
271
|
+
if (index !== false) {
|
|
272
|
+
if (path === index) {
|
|
273
|
+
add("/", path);
|
|
274
|
+
}
|
|
275
|
+
else if (path.endsWith(`/${index}`)) {
|
|
276
|
+
const directory = path.slice(0, -(index.length + 1));
|
|
277
|
+
add(`/${directory}`, path);
|
|
278
|
+
add(`/${directory}/`, path);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
const collisions = [...routes.entries()]
|
|
283
|
+
.filter(([, owners]) => owners.size > 1)
|
|
284
|
+
.map(([route, owners]) => ({ route, paths: [...owners].toSorted() }))
|
|
285
|
+
.toSorted((left, right) => left.route.localeCompare(right.route));
|
|
286
|
+
const firstByFoldedRoute = new Map();
|
|
287
|
+
const caseCollisions = [];
|
|
288
|
+
for (const [route, owners] of routes) {
|
|
289
|
+
const folded = route.toLowerCase();
|
|
290
|
+
const pathsForRoute = [...owners].toSorted();
|
|
291
|
+
const sibling = firstByFoldedRoute.get(folded);
|
|
292
|
+
if (sibling && sibling.route !== route) {
|
|
293
|
+
caseCollisions.push({
|
|
294
|
+
route,
|
|
295
|
+
siblingRoute: sibling.route,
|
|
296
|
+
paths: [...new Set([...sibling.paths, ...pathsForRoute])].toSorted(),
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
else if (!sibling) {
|
|
300
|
+
firstByFoldedRoute.set(folded, { route, paths: pathsForRoute });
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
const canonicalOwnership = new Map();
|
|
304
|
+
for (const [route, owners] of routes) {
|
|
305
|
+
const canonicalRoute = canonicalizePublishRequestedRoute(route);
|
|
306
|
+
const canonicalOwners = canonicalOwnership.get(canonicalRoute) ?? new Set();
|
|
307
|
+
for (const owner of owners)
|
|
308
|
+
canonicalOwners.add(owner);
|
|
309
|
+
canonicalOwnership.set(canonicalRoute, canonicalOwners);
|
|
310
|
+
}
|
|
311
|
+
const routeOwnership = [...canonicalOwnership.entries()]
|
|
312
|
+
.flatMap(([requestedRoute, owners]) => [...owners].map((artifactPath) => ({ requestedRoute, artifactPath })))
|
|
313
|
+
.toSorted((left, right) => left.requestedRoute.localeCompare(right.requestedRoute) ||
|
|
314
|
+
left.artifactPath.localeCompare(right.artifactPath));
|
|
315
|
+
return {
|
|
316
|
+
collisions,
|
|
317
|
+
caseCollisions: caseCollisions.toSorted((left, right) => left.route.localeCompare(right.route)),
|
|
318
|
+
routeOwnership,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
206
321
|
export function resolveManifestFileCap(maxFiles) {
|
|
207
322
|
return Math.min(MANIFEST_MAX_FILES_CEILING, maxFiles && maxFiles > 0 ? maxFiles : MANIFEST_MAX_FILES_CEILING);
|
|
208
323
|
}
|
|
@@ -374,8 +489,6 @@ export const ANONYMOUS_SERVE_CONTENT_TYPES = [
|
|
|
374
489
|
"application/rss+xml",
|
|
375
490
|
"application/atom+xml",
|
|
376
491
|
];
|
|
377
|
-
const LINE_SPLIT_PATTERN = /\r?\n/;
|
|
378
|
-
const BASIC_AUTH_HEADER_PATTERN = /^basic-auth\s*:/i;
|
|
379
492
|
function extensionForPath(filePath) {
|
|
380
493
|
const normalized = normalizePublishPath(filePath).toLowerCase();
|
|
381
494
|
const fileName = normalized.split("/").at(-1) ?? normalized;
|
|
@@ -449,45 +562,3 @@ export function filterFreePlanManifest(files, options) {
|
|
|
449
562
|
}
|
|
450
563
|
return { allowed, ignored };
|
|
451
564
|
}
|
|
452
|
-
// Free-plan `_headers` compile policy (spec "Plans And Limits", amended
|
|
453
|
-
// 2026-06-10): custom response headers stay FREE; the Basic-Auth header
|
|
454
|
-
// MECHANISM is the paid feature. Basic-Auth operations are dropped from free
|
|
455
|
-
// compiles with a `free_headers_basic_auth_disabled` warning; every other
|
|
456
|
-
// header operation passes through untouched.
|
|
457
|
-
export function filterFreePlanHeaders(content) {
|
|
458
|
-
const output = [];
|
|
459
|
-
let droppedBasicAuthCount = 0;
|
|
460
|
-
let pendingMatcher = null;
|
|
461
|
-
let matcherEmitted = false;
|
|
462
|
-
const emitOperation = (rawLine) => {
|
|
463
|
-
if (pendingMatcher !== null && !matcherEmitted) {
|
|
464
|
-
output.push(pendingMatcher);
|
|
465
|
-
matcherEmitted = true;
|
|
466
|
-
}
|
|
467
|
-
output.push(rawLine);
|
|
468
|
-
};
|
|
469
|
-
for (const rawLine of content.split(LINE_SPLIT_PATTERN)) {
|
|
470
|
-
const trimmed = rawLine.trim();
|
|
471
|
-
if (!trimmed || trimmed.startsWith("#")) {
|
|
472
|
-
continue;
|
|
473
|
-
}
|
|
474
|
-
if (!rawLine.startsWith(" ") && !rawLine.startsWith("\t")) {
|
|
475
|
-
pendingMatcher = trimmed;
|
|
476
|
-
matcherEmitted = false;
|
|
477
|
-
continue;
|
|
478
|
-
}
|
|
479
|
-
if (pendingMatcher === null) {
|
|
480
|
-
continue;
|
|
481
|
-
}
|
|
482
|
-
if (BASIC_AUTH_HEADER_PATTERN.test(trimmed)) {
|
|
483
|
-
droppedBasicAuthCount += 1;
|
|
484
|
-
}
|
|
485
|
-
else {
|
|
486
|
-
emitOperation(rawLine);
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
return {
|
|
490
|
-
headers: output.length > 0 ? `${output.join("\n")}\n` : undefined,
|
|
491
|
-
droppedBasicAuthCount,
|
|
492
|
-
};
|
|
493
|
-
}
|
|
@@ -63,20 +63,15 @@ export declare const qk: {
|
|
|
63
63
|
spaceDomains: (id: string) => readonly ["spaces", string, "domains"];
|
|
64
64
|
spaceRuntimeStatus: (id: string) => readonly ["spaces", string, "runtime"];
|
|
65
65
|
spaceApplyState: (id: string) => readonly ["spaces", string, "apply-state"];
|
|
66
|
-
spaceZero: (id: string) => readonly ["spaces", string, "zero"];
|
|
67
66
|
spaceZeroDb: (id: string) => readonly ["spaces", string, "zero", "db"];
|
|
68
|
-
|
|
67
|
+
spaceRuntimeLogs: (id: string, limit: number) => readonly ["spaces", string, "logs", "runtime", number];
|
|
69
68
|
spaceZeroRealtimeEvents: (id: string, limit: number) => readonly ["spaces", string, "zero", "realtime", "events", number];
|
|
70
69
|
spaceVariables: (id: string) => readonly ["spaces", string, "variables"];
|
|
71
70
|
spaceCertificates: (id: string) => readonly ["spaces", string, "runtime", "certificates"];
|
|
72
71
|
spaceLogs: (id: string, kind: "build" | "access" | "runtime") => readonly ["spaces", string, "logs", "build" | "runtime" | "access"];
|
|
73
72
|
spaceAnalytics: (id: string, window: string) => readonly ["spaces", string, "analytics", string];
|
|
74
|
-
spaceFirewall: (id: string) => readonly ["spaces", string, "access", "firewall"];
|
|
75
|
-
spaceAccessViews: (id: string, since?: string) => readonly ["spaces", string, "access", "views", string];
|
|
76
|
-
teamServiceTokens: (teamId: string) => readonly ["teams", string, "access", "service-tokens"];
|
|
77
73
|
teamAgentHandoffs: (teamId: string) => readonly ["teams", string, "agent-handoffs"];
|
|
78
74
|
teamAccessDefaults: (teamId: string) => readonly ["teams", string, "access", "defaults"];
|
|
79
|
-
teamAccessConnections: (teamId: string) => readonly ["teams", string, "access", "connections"];
|
|
80
75
|
domain: (teamId: string, ref: string) => readonly ["teams", string, "domains", string];
|
|
81
76
|
domainDiagnostics: (teamId: string, domainId: string) => readonly ["teams", string, "domains", string, "diagnostics"];
|
|
82
77
|
domainDnsCapabilities: (domainId: string) => readonly ["domains", string, "dns", "capabilities"];
|
package/dist/utils/query-keys.js
CHANGED
|
@@ -81,20 +81,15 @@ export const qk = {
|
|
|
81
81
|
spaceDomains: (id) => ["spaces", id, "domains"],
|
|
82
82
|
spaceRuntimeStatus: (id) => ["spaces", id, "runtime"],
|
|
83
83
|
spaceApplyState: (id) => ["spaces", id, "apply-state"],
|
|
84
|
-
spaceZero: (id) => ["spaces", id, "zero"],
|
|
85
84
|
spaceZeroDb: (id) => ["spaces", id, "zero", "db"],
|
|
86
|
-
|
|
85
|
+
spaceRuntimeLogs: (id, limit) => ["spaces", id, "logs", "runtime", limit],
|
|
87
86
|
spaceZeroRealtimeEvents: (id, limit) => ["spaces", id, "zero", "realtime", "events", limit],
|
|
88
87
|
spaceVariables: (id) => ["spaces", id, "variables"],
|
|
89
88
|
spaceCertificates: (id) => ["spaces", id, "runtime", "certificates"],
|
|
90
89
|
spaceLogs: (id, kind) => ["spaces", id, "logs", kind],
|
|
91
90
|
spaceAnalytics: (id, window) => ["spaces", id, "analytics", window],
|
|
92
|
-
spaceFirewall: (id) => ["spaces", id, "access", "firewall"],
|
|
93
|
-
spaceAccessViews: (id, since) => ["spaces", id, "access", "views", since ?? ""],
|
|
94
|
-
teamServiceTokens: (teamId) => ["teams", teamId, "access", "service-tokens"],
|
|
95
91
|
teamAgentHandoffs: (teamId) => ["teams", teamId, "agent-handoffs"],
|
|
96
92
|
teamAccessDefaults: (teamId) => ["teams", teamId, "access", "defaults"],
|
|
97
|
-
teamAccessConnections: (teamId) => ["teams", teamId, "access", "connections"],
|
|
98
93
|
domain: (teamId, ref) => ["teams", teamId, "domains", ref],
|
|
99
94
|
domainDiagnostics: (teamId, domainId) => ["teams", teamId, "domains", domainId, "diagnostics"],
|
|
100
95
|
domainDnsCapabilities: (domainId) => ["domains", domainId, "dns", "capabilities"],
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type Grant } from "../contracts/grants.js";
|
|
1
2
|
import { type SpaceConfig, type SpaceConfigFile } from "../contracts/space-config.js";
|
|
2
3
|
export type SpaceConfigBlockingIssueCode = "config_file_too_large" | "config_templates_over_limit" | "config_meta_too_long" | "config_invalid" | "inject_snippet_too_large" | "inject_invalid";
|
|
3
4
|
export type SpaceConfigIssue = {
|
|
@@ -8,6 +9,9 @@ export type SpaceConfigIssue = {
|
|
|
8
9
|
path?: string;
|
|
9
10
|
details?: Record<string, unknown>;
|
|
10
11
|
};
|
|
12
|
+
export declare const CONFIG_PUBLIC_GRANT_COMPILED_ID = "config:access.public";
|
|
13
|
+
/** Compiles the file-only access declaration; activation decides which Version is live. */
|
|
14
|
+
export declare function compileSpaceConfigPublicGrant(fileConfig: Pick<SpaceConfigFile, "access"> | null | undefined): Grant | null;
|
|
11
15
|
type SpaceConfigBlockingIssue = SpaceConfigIssue & {
|
|
12
16
|
code: SpaceConfigBlockingIssueCode;
|
|
13
17
|
};
|
|
@@ -16,7 +20,7 @@ type SpaceConfigBlockingIssue = SpaceConfigIssue & {
|
|
|
16
20
|
* `config`): the 16 KB compiled-overlay cap plus the shared shape caps.
|
|
17
21
|
* Returns the first blocking issue, or null when the overlay is acceptable.
|
|
18
22
|
*/
|
|
19
|
-
export declare function spaceConfigOverlayIssue(config: SpaceConfig): SpaceConfigBlockingIssue | null;
|
|
23
|
+
export declare function spaceConfigOverlayIssue(config: SpaceConfig | Record<string, unknown>): SpaceConfigBlockingIssue | null;
|
|
20
24
|
export declare function stripJsonComments(raw: string): string;
|
|
21
25
|
export type ParsedSpaceConfigFile = {
|
|
22
26
|
/** Parsed config (cap- and shape-valid keys only); null when unusable. */
|
|
@@ -1,8 +1,35 @@
|
|
|
1
|
+
import { CONFIG_PUBLIC_GRANT_SOURCE_REFERENCE, grantSchema, } from "../contracts/grants.js";
|
|
1
2
|
import { SPACE_CONFIG_CAPS, SPACE_CONFIG_CANONICAL_FILE, SPACE_CONFIG_PRIVATE_FILES, spaceConfigFileSchema, spaceConfigSchema, } from "../contracts/space-config.js";
|
|
2
3
|
const TEXT_BYTES = new TextEncoder();
|
|
4
|
+
export const CONFIG_PUBLIC_GRANT_COMPILED_ID = "config:access.public";
|
|
3
5
|
function byteLength(value) {
|
|
4
6
|
return TEXT_BYTES.encode(value).length;
|
|
5
7
|
}
|
|
8
|
+
/** Compiles the file-only access declaration; activation decides which Version is live. */
|
|
9
|
+
export function compileSpaceConfigPublicGrant(fileConfig) {
|
|
10
|
+
const patterns = fileConfig?.access?.public ?? [];
|
|
11
|
+
const include = patterns.filter((pattern) => !pattern.startsWith("!"));
|
|
12
|
+
if (include.length === 0)
|
|
13
|
+
return null;
|
|
14
|
+
return grantSchema.parse({
|
|
15
|
+
id: CONFIG_PUBLIC_GRANT_COMPILED_ID,
|
|
16
|
+
name: "Public paths from sf.jsonc",
|
|
17
|
+
audience: { kind: "public" },
|
|
18
|
+
resources: {
|
|
19
|
+
include,
|
|
20
|
+
exclude: patterns
|
|
21
|
+
.filter((pattern) => pattern.startsWith("!"))
|
|
22
|
+
.map((pattern) => pattern.slice(1)),
|
|
23
|
+
},
|
|
24
|
+
capabilities: ["page.view"],
|
|
25
|
+
constraints: {},
|
|
26
|
+
target: { kind: "live" },
|
|
27
|
+
source: {
|
|
28
|
+
kind: "config",
|
|
29
|
+
reference: CONFIG_PUBLIC_GRANT_SOURCE_REFERENCE,
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
}
|
|
6
33
|
function capIssues(candidate) {
|
|
7
34
|
const issues = [];
|
|
8
35
|
const templates = candidate.templates;
|
|
@@ -102,6 +129,14 @@ function capIssues(candidate) {
|
|
|
102
129
|
* Returns the first blocking issue, or null when the overlay is acceptable.
|
|
103
130
|
*/
|
|
104
131
|
export function spaceConfigOverlayIssue(config) {
|
|
132
|
+
if (Object.prototype.hasOwnProperty.call(config, "access")) {
|
|
133
|
+
return {
|
|
134
|
+
severity: "error",
|
|
135
|
+
code: "config_invalid",
|
|
136
|
+
message: "access is only valid in the committed sf.jsonc file.",
|
|
137
|
+
path: "access",
|
|
138
|
+
};
|
|
139
|
+
}
|
|
105
140
|
const encoded = JSON.stringify(config);
|
|
106
141
|
if (byteLength(encoded) > SPACE_CONFIG_CAPS.overlayBytes) {
|
|
107
142
|
return {
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
export declare const PHP_LIKE_EXTENSIONS: string[];
|
|
2
|
+
export declare const INTERNAL_REDIRECT_RESPONSE_HEADERS: string[];
|
|
2
3
|
export declare const PLATFORM_MANAGED_RESPONSE_HEADERS: string[];
|
|
4
|
+
/** Reserved response-header namespaces owned by the platform's own serving signature. */
|
|
5
|
+
export declare const PLATFORM_MANAGED_RESPONSE_HEADER_PREFIXES: string[];
|
|
6
|
+
/** True when user rules may neither set nor remove this response header. */
|
|
7
|
+
export declare function isPlatformManagedResponseHeader(name: string): boolean;
|
|
3
8
|
export declare const NON_IMMUTABLE_EXTENSIONS: string[];
|
|
4
9
|
export declare const GUARANTEED_IMMUTABLE_PREFIXES: string[];
|
|
5
10
|
/** Lowercased extension without the leading dot, posix semantics. Dotfiles have no extension. */
|
|
@@ -7,13 +7,47 @@
|
|
|
7
7
|
// (static-runtime-policy.fixtures.json). Do not add platform/filesystem
|
|
8
8
|
// behavior here.
|
|
9
9
|
export const PHP_LIKE_EXTENSIONS = ["php", "php3", "php4", "php5", "php7", "php8", "phtml", "phar"];
|
|
10
|
+
// The internal-redirect/sendfile family: headers that do not describe a response, they
|
|
11
|
+
// instruct the web server to produce a different one. Under nginx+php-fpm
|
|
12
|
+
// X-Accel-Redirect (and every vendor spelling of it) turns a response into a server-side
|
|
13
|
+
// read of an arbitrary internal path — a private-path disclosure primitive — while
|
|
14
|
+
// X-Accel-Limit-Rate pins the serving worker for the length of the transfer.
|
|
15
|
+
//
|
|
16
|
+
// These are dangerous from ANY origin we do not control, so this family is factored out
|
|
17
|
+
// of the platform-managed list below: a relay lane (runtime/engine/runtime/proxy.php)
|
|
18
|
+
// must drop exactly this set from an untrusted upstream while still relaying the
|
|
19
|
+
// ordinary response headers (location, content-encoding, content-range, allow) that the
|
|
20
|
+
// rest of the managed list contains.
|
|
21
|
+
export const INTERNAL_REDIRECT_RESPONSE_HEADERS = [
|
|
22
|
+
"x-accel-buffering",
|
|
23
|
+
"x-accel-charset",
|
|
24
|
+
"x-accel-expires",
|
|
25
|
+
"x-accel-limit-rate",
|
|
26
|
+
"x-accel-redirect",
|
|
27
|
+
"x-lighttpd-send-file",
|
|
28
|
+
"x-lighttpd-sendfile",
|
|
29
|
+
"x-lighttpd-sendfile2",
|
|
30
|
+
"x-reproxy-url",
|
|
31
|
+
"x-sendfile",
|
|
32
|
+
];
|
|
10
33
|
// Rejected/platform-managed response headers (internal-docs/platform.md "Routing And
|
|
11
|
-
// Headers"): user `_headers` rules can never set or remove these. The
|
|
12
|
-
// internal-redirect/sendfile family
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
34
|
+
// Headers"): user `_headers` rules can never set or remove these. The
|
|
35
|
+
// internal-redirect/sendfile family above is spread in, so it stays a strict subset —
|
|
36
|
+
// anything the proxy lane refuses to relay is also refused from `_headers`. The rest is
|
|
37
|
+
// the connection and transport surface: hop-by-hop names (Connection, Keep-Alive, TE,
|
|
38
|
+
// Trailer, Transfer-Encoding, Upgrade, Proxy-*), framing, and the edge-cache trio.
|
|
39
|
+
//
|
|
40
|
+
// `isPlatformManagedResponseHeader` — not this list alone — is the policy: the reserved
|
|
41
|
+
// `x-spacefast-*` / `x-stattic-*` namespaces below carry the platform's own response
|
|
42
|
+
// signature (X-Spacefast-Runtime, X-Spacefast-Version, X-Spacefast-Reason, …), so user
|
|
43
|
+
// rules can never forge or clobber them.
|
|
44
|
+
//
|
|
45
|
+
// Every enforcement point derives from here: the `_headers` compiler in
|
|
46
|
+
// @spacefast/routing imports it directly, the PHP runtime maps and prefixes are
|
|
47
|
+
// generated from it into runtime/engine/shared/safety.php, and the Rust authorities
|
|
48
|
+
// (crates/stattic-runtime-core policy.rs, crates/stattic-zero-runner
|
|
49
|
+
// response_headers.rs) are drift-guarded against it by
|
|
50
|
+
// apps/control-plane/src/runtime/php-policy-parity.test.ts.
|
|
17
51
|
export const PLATFORM_MANAGED_RESPONSE_HEADERS = [
|
|
18
52
|
"accept-ranges",
|
|
19
53
|
"age",
|
|
@@ -28,21 +62,31 @@ export const PLATFORM_MANAGED_RESPONSE_HEADERS = [
|
|
|
28
62
|
"cookie",
|
|
29
63
|
"date",
|
|
30
64
|
"host",
|
|
65
|
+
"keep-alive",
|
|
31
66
|
"location",
|
|
32
67
|
"netlify-cdn-cache-control",
|
|
68
|
+
"proxy-authenticate",
|
|
69
|
+
"proxy-authorization",
|
|
33
70
|
"server",
|
|
34
71
|
"set-cookie",
|
|
35
72
|
"strict-transport-security",
|
|
36
73
|
"surrogate-control",
|
|
74
|
+
"te",
|
|
37
75
|
"trailer",
|
|
38
76
|
"transfer-encoding",
|
|
39
77
|
"upgrade",
|
|
40
78
|
"vary",
|
|
41
|
-
|
|
42
|
-
"x-accel-redirect",
|
|
43
|
-
"x-lighttpd-send-file",
|
|
44
|
-
"x-sendfile",
|
|
79
|
+
...INTERNAL_REDIRECT_RESPONSE_HEADERS,
|
|
45
80
|
];
|
|
81
|
+
/** Reserved response-header namespaces owned by the platform's own serving signature. */
|
|
82
|
+
export const PLATFORM_MANAGED_RESPONSE_HEADER_PREFIXES = ["x-spacefast-", "x-stattic-"];
|
|
83
|
+
const PLATFORM_MANAGED_RESPONSE_HEADER_SET = new Set(PLATFORM_MANAGED_RESPONSE_HEADERS);
|
|
84
|
+
/** True when user rules may neither set nor remove this response header. */
|
|
85
|
+
export function isPlatformManagedResponseHeader(name) {
|
|
86
|
+
const lower = name.trim().toLowerCase();
|
|
87
|
+
return (PLATFORM_MANAGED_RESPONSE_HEADER_SET.has(lower) ||
|
|
88
|
+
PLATFORM_MANAGED_RESPONSE_HEADER_PREFIXES.some((prefix) => lower.startsWith(prefix)));
|
|
89
|
+
}
|
|
46
90
|
export const NON_IMMUTABLE_EXTENSIONS = ["html", "htm", "php", "txt", "xml"];
|
|
47
91
|
const PHP_LIKE_EXTENSION_SET = new Set(PHP_LIKE_EXTENSIONS);
|
|
48
92
|
const NON_IMMUTABLE_EXTENSION_SET = new Set(NON_IMMUTABLE_EXTENSIONS);
|
|
@@ -24,6 +24,9 @@ export declare class UploadTargetError extends Error {
|
|
|
24
24
|
readonly status: number;
|
|
25
25
|
constructor(target: UploadTarget, status: number, message: string);
|
|
26
26
|
}
|
|
27
|
+
export declare function uploadPageHasRemainder(page: UploadSession): boolean;
|
|
28
|
+
export declare const UPLOAD_STALLED_RESUME_MAX_ATTEMPTS = 3;
|
|
29
|
+
export declare function uploadQueueAdvanced(before: UploadSession, after: UploadSession | null): boolean;
|
|
27
30
|
export declare function executeUploadSession(input: {
|
|
28
31
|
session: UploadSession;
|
|
29
32
|
targets?: readonly UploadTarget[] | undefined;
|
|
@@ -8,6 +8,37 @@ export class UploadTargetError extends Error {
|
|
|
8
8
|
this.name = "UploadTargetError";
|
|
9
9
|
}
|
|
10
10
|
}
|
|
11
|
+
// Manifest paging policy — ONE definition, shared by every uploader (the CLI
|
|
12
|
+
// and the control plane's inline uploader).
|
|
13
|
+
//
|
|
14
|
+
// `targets` is a single instruction page, bounded by the instruction payload
|
|
15
|
+
// budget; `summary.upload` counts the whole declared manifest. A short page
|
|
16
|
+
// therefore means a remainder that only `resume` hands out, and an uploader
|
|
17
|
+
// that stops at the first page finalizes a version the runtime rejects with
|
|
18
|
+
// `version_upload_incomplete`. A complete page means there is nothing left to
|
|
19
|
+
// ask for — don't re-resume to double-check the uploads, since the runtime
|
|
20
|
+
// verifies size and sha256 on every PUT and finalize independently enumerates
|
|
21
|
+
// anything still missing.
|
|
22
|
+
export function uploadPageHasRemainder(page) {
|
|
23
|
+
return page.summary.upload > page.targets.length;
|
|
24
|
+
}
|
|
25
|
+
// Resume mints the next page from runtime session truth, so a converging
|
|
26
|
+
// session always hands back different work. A page that repeats the previous
|
|
27
|
+
// one is a session that is not draining; retry it a bounded number of times,
|
|
28
|
+
// then fail loudly rather than page forever.
|
|
29
|
+
export const UPLOAD_STALLED_RESUME_MAX_ATTEMPTS = 3;
|
|
30
|
+
export function uploadQueueAdvanced(before, after) {
|
|
31
|
+
if (!after || after.targets.length === 0) {
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
if (after.targets.length < before.targets.length) {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
if (after.targets.length > before.targets.length) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
return after.targets.some((target, index) => target.path !== before.targets[index]?.path);
|
|
41
|
+
}
|
|
11
42
|
async function runConcurrent(items, concurrency, fn) {
|
|
12
43
|
let nextIndex = 0;
|
|
13
44
|
const runWorker = async () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spacefast/common",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.13",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Shared TypeScript contracts and utilities for Spacefast packages.",
|
|
6
6
|
"repository": {
|
|
@@ -93,6 +93,7 @@
|
|
|
93
93
|
"dependencies": {
|
|
94
94
|
"@tanstack/react-query": "^5.101.4",
|
|
95
95
|
"culori": "^4.0.2",
|
|
96
|
+
"js-yaml": "^4.1.1",
|
|
96
97
|
"smol-toml": "^1.7.0",
|
|
97
98
|
"zod": "^4.4.3"
|
|
98
99
|
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import type { RuntimeRule } from "../contracts/access.js";
|
|
2
|
-
export declare const ACCESS_GRANT_GATE_RULE_ID = "access-grant-gate";
|
|
3
|
-
export declare const ACCESS_GRANT_ADMISSION_RULE_ID = "access-grant-admission";
|
|
4
|
-
export type AccessGrantExposure = {
|
|
5
|
-
anonymousAccess: "gated" | "gated_with_exceptions" | "partly_public" | "shadowed";
|
|
6
|
-
earlierAnonymousAllowRules: number;
|
|
7
|
-
earlierManagedAccessRules: number;
|
|
8
|
-
precedingTerminalRules: number;
|
|
9
|
-
};
|
|
10
|
-
export declare function withAccessGrantGate(rules: RuntimeRule[], grant: string): RuntimeRule[];
|
|
11
|
-
export declare function withoutAccessGrantRule(rules: RuntimeRule[], selectedIndex: number): RuntimeRule[];
|
|
12
|
-
export declare function isAccessGrantAdmissionRule(rule: RuntimeRule): boolean;
|
|
13
|
-
export declare function isAccessGrantFallbackRule(rule: RuntimeRule): boolean;
|
|
14
|
-
export declare function accessGrantExposure(rules: RuntimeRule[]): AccessGrantExposure;
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
// `sf access grant` and the dashboard's Grant access card share one canonical
|
|
2
|
-
// pair: an authenticated admission allow plus an anonymous fallback challenge.
|
|
3
|
-
// Repeated grants widen both rules' OR-list. The split is essential under
|
|
4
|
-
// first-match evaluation: the admission can pass before managed challenges,
|
|
5
|
-
// while the fallback closes otherwise-public traffic after those challenges.
|
|
6
|
-
export const ACCESS_GRANT_GATE_RULE_ID = "access-grant-gate";
|
|
7
|
-
export const ACCESS_GRANT_ADMISSION_RULE_ID = "access-grant-admission";
|
|
8
|
-
export function withAccessGrantGate(rules, grant) {
|
|
9
|
-
// Raw access writes replace only the unmanaged lane. GET/PUT responses may
|
|
10
|
-
// include server-owned managed blocks, but sending those blocks back would
|
|
11
|
-
// be rejected and could let a client accidentally claim their ownership.
|
|
12
|
-
// Reserved access-grant rules are rebuilt as one canonical pair below.
|
|
13
|
-
const unmanagedRules = rules.filter((rule) => rule.managedBy === undefined &&
|
|
14
|
-
rule.id !== ACCESS_GRANT_ADMISSION_RULE_ID &&
|
|
15
|
-
rule.id !== ACCESS_GRANT_GATE_RULE_ID);
|
|
16
|
-
const existingRequiredGrants = rules.find((rule) => rule.id === ACCESS_GRANT_ADMISSION_RULE_ID)
|
|
17
|
-
?.auth?.requiredGrants;
|
|
18
|
-
const fallbackRequiredGrants = rules.find((rule) => rule.id === ACCESS_GRANT_GATE_RULE_ID)?.auth
|
|
19
|
-
?.requiredGrants;
|
|
20
|
-
const requiredGrants = [
|
|
21
|
-
...new Set([...(existingRequiredGrants ?? fallbackRequiredGrants ?? []), grant]),
|
|
22
|
-
];
|
|
23
|
-
return [
|
|
24
|
-
...unmanagedRules,
|
|
25
|
-
{
|
|
26
|
-
id: ACCESS_GRANT_ADMISSION_RULE_ID,
|
|
27
|
-
match: {},
|
|
28
|
-
effect: "allow",
|
|
29
|
-
auth: { requiredGrants },
|
|
30
|
-
reasonCode: "identity_grant_admitted",
|
|
31
|
-
},
|
|
32
|
-
{
|
|
33
|
-
id: ACCESS_GRANT_GATE_RULE_ID,
|
|
34
|
-
match: {},
|
|
35
|
-
effect: "challenge",
|
|
36
|
-
auth: { requiredGrants },
|
|
37
|
-
reasonCode: "identity_grant_required",
|
|
38
|
-
message: "Sign in with an approved identity to continue.",
|
|
39
|
-
},
|
|
40
|
-
];
|
|
41
|
-
}
|
|
42
|
-
export function withoutAccessGrantRule(rules, selectedIndex) {
|
|
43
|
-
const selectedRule = rules[selectedIndex];
|
|
44
|
-
const removesAccessGrant = selectedRule?.id === ACCESS_GRANT_ADMISSION_RULE_ID ||
|
|
45
|
-
selectedRule?.id === ACCESS_GRANT_GATE_RULE_ID;
|
|
46
|
-
return rules.filter((rule, index) => rule.managedBy === undefined &&
|
|
47
|
-
(removesAccessGrant
|
|
48
|
-
? rule.id !== ACCESS_GRANT_ADMISSION_RULE_ID && rule.id !== ACCESS_GRANT_GATE_RULE_ID
|
|
49
|
-
: index !== selectedIndex));
|
|
50
|
-
}
|
|
51
|
-
export function isAccessGrantAdmissionRule(rule) {
|
|
52
|
-
return (rule.id === ACCESS_GRANT_ADMISSION_RULE_ID &&
|
|
53
|
-
rule.effect === "allow" &&
|
|
54
|
-
Object.keys(rule.match).length === 0 &&
|
|
55
|
-
rule.auth !== undefined);
|
|
56
|
-
}
|
|
57
|
-
export function isAccessGrantFallbackRule(rule) {
|
|
58
|
-
return (rule.id === ACCESS_GRANT_GATE_RULE_ID &&
|
|
59
|
-
rule.effect === "challenge" &&
|
|
60
|
-
Object.keys(rule.match).length === 0 &&
|
|
61
|
-
rule.auth !== undefined);
|
|
62
|
-
}
|
|
63
|
-
// The admission allow protects approved identities without preempting the
|
|
64
|
-
// existing password/share/service challenge. The final fallback gate protects
|
|
65
|
-
// public fallthrough. A terminal rule from an earlier effective lane can still
|
|
66
|
-
// stop evaluation before admission; report that explicitly instead of claiming
|
|
67
|
-
// the space is simply gated.
|
|
68
|
-
export function accessGrantExposure(rules) {
|
|
69
|
-
const gateIndex = rules.findIndex(isAccessGrantFallbackRule);
|
|
70
|
-
const admissionIndex = rules.findIndex(isAccessGrantAdmissionRule);
|
|
71
|
-
const earlierRules = gateIndex === -1 ? rules : rules.slice(0, gateIndex);
|
|
72
|
-
const rulesBeforeAdmission = admissionIndex === -1 ? earlierRules : rules.slice(0, admissionIndex);
|
|
73
|
-
const precedingTerminalRules = rulesBeforeAdmission.filter((rule) => rule.effect === "challenge" || (rule.effect === "deny" && rule.auth === undefined)).length;
|
|
74
|
-
const earlierAnonymousAllowRules = earlierRules.filter((rule) => rule.effect === "allow" && !rule.auth).length;
|
|
75
|
-
const earlierManagedAccessRules = earlierRules.filter((rule) => rule.managedBy !== undefined && rule.effect === "allow").length;
|
|
76
|
-
return {
|
|
77
|
-
anonymousAccess: precedingTerminalRules > 0
|
|
78
|
-
? "shadowed"
|
|
79
|
-
: earlierAnonymousAllowRules > 0
|
|
80
|
-
? "partly_public"
|
|
81
|
-
: earlierManagedAccessRules > 0
|
|
82
|
-
? "gated_with_exceptions"
|
|
83
|
-
: "gated",
|
|
84
|
-
earlierAnonymousAllowRules,
|
|
85
|
-
earlierManagedAccessRules,
|
|
86
|
-
precedingTerminalRules,
|
|
87
|
-
};
|
|
88
|
-
}
|