@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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { load as parseYaml } from "js-yaml";
|
|
1
2
|
import { parse as parseToml } from "smol-toml";
|
|
2
3
|
export const BUILD_ANALYZER_CANDIDATE_ROOTS = ["apps", "packages", "sites", "docs"];
|
|
3
4
|
const PACKAGE_MANAGER_ONLY_FILES = new Set([
|
|
@@ -7,6 +8,8 @@ const PACKAGE_MANAGER_ONLY_FILES = new Set([
|
|
|
7
8
|
"pnpm-lock.yaml",
|
|
8
9
|
"yarn.lock",
|
|
9
10
|
"package-lock.json",
|
|
11
|
+
"Gemfile",
|
|
12
|
+
"Gemfile.lock",
|
|
10
13
|
]);
|
|
11
14
|
export function hasWorkspaceSignal(fileNames, packageJsonText) {
|
|
12
15
|
const names = new Set(fileNames);
|
|
@@ -102,6 +105,20 @@ function parseTomlObject(value) {
|
|
|
102
105
|
return null;
|
|
103
106
|
}
|
|
104
107
|
}
|
|
108
|
+
function parseYamlObject(value) {
|
|
109
|
+
if (!value) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
const parsed = parseYaml(value);
|
|
114
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
115
|
+
? parsed
|
|
116
|
+
: null;
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
105
122
|
export function stringField(value, key) {
|
|
106
123
|
const field = value?.[key];
|
|
107
124
|
return typeof field === "string" && field.trim() ? field.trim() : null;
|
|
@@ -628,9 +645,8 @@ const FRAMEWORKS = [
|
|
|
628
645
|
},
|
|
629
646
|
{
|
|
630
647
|
preset: "jekyll",
|
|
631
|
-
configFiles: ["_config.yml", "_config.yaml"],
|
|
648
|
+
configFiles: ["_config.yml", "_config.yaml", "_config.toml"],
|
|
632
649
|
outputDirectory: "_site",
|
|
633
|
-
defaultBuildCommand: "bundle exec jekyll build",
|
|
634
650
|
},
|
|
635
651
|
{
|
|
636
652
|
preset: "mkdocs",
|
|
@@ -675,6 +691,26 @@ function outputDirectoryForFramework(frameworkPreset, files) {
|
|
|
675
691
|
return framework.outputDirectory;
|
|
676
692
|
return files.has("dist") ? "dist" : null;
|
|
677
693
|
}
|
|
694
|
+
const JEKYLL_BUNDLE_PATH = ".spacefast/cache/jekyll-bundle";
|
|
695
|
+
const JEKYLL_GEM_HOME = ".spacefast/cache/jekyll-gems";
|
|
696
|
+
function jekyllInstallCommand(files) {
|
|
697
|
+
return files.has("Gemfile")
|
|
698
|
+
? `BUNDLE_PATH=${JEKYLL_BUNDLE_PATH} bundle install`
|
|
699
|
+
: `GEM_HOME=${JEKYLL_GEM_HOME} gem install jekyll --no-document`;
|
|
700
|
+
}
|
|
701
|
+
function jekyllConfigFile(files) {
|
|
702
|
+
return ["_config.yml", "_config.yaml", "_config.toml"].find((file) => files.has(file));
|
|
703
|
+
}
|
|
704
|
+
function jekyllBuildCommand(files) {
|
|
705
|
+
const command = files.has("Gemfile")
|
|
706
|
+
? `JEKYLL_ENV=production BUNDLE_PATH=${JEKYLL_BUNDLE_PATH} bundle exec jekyll build`
|
|
707
|
+
: `JEKYLL_ENV=production GEM_HOME=${JEKYLL_GEM_HOME} ${JEKYLL_GEM_HOME}/bin/jekyll build`;
|
|
708
|
+
const configFile = jekyllConfigFile(files);
|
|
709
|
+
return configFile ? `${command} --config ${configFile}` : command;
|
|
710
|
+
}
|
|
711
|
+
function frameworkInstallCommand(frameworkPreset, files) {
|
|
712
|
+
return frameworkPreset === "jekyll" ? jekyllInstallCommand(files) : null;
|
|
713
|
+
}
|
|
678
714
|
function installCommandForManager(packageManager, packageJson) {
|
|
679
715
|
switch (packageManager) {
|
|
680
716
|
case "bun":
|
|
@@ -687,13 +723,15 @@ function installCommandForManager(packageManager, packageJson) {
|
|
|
687
723
|
return packageJson ? "npm ci" : null;
|
|
688
724
|
}
|
|
689
725
|
}
|
|
690
|
-
function buildCommandForDefaults(packageJson, run, frameworkPreset) {
|
|
726
|
+
function buildCommandForDefaults(packageJson, run, frameworkPreset, files) {
|
|
691
727
|
if (packageJson?.scripts?.build)
|
|
692
728
|
return `${run} build`;
|
|
729
|
+
if (frameworkPreset === "jekyll")
|
|
730
|
+
return jekyllBuildCommand(files);
|
|
693
731
|
return frameworkForPreset(frameworkPreset)?.defaultBuildCommand ?? null;
|
|
694
732
|
}
|
|
695
|
-
function packageDefaults(
|
|
696
|
-
const packageJson = parsePackageJson(
|
|
733
|
+
function packageDefaults(input, files) {
|
|
734
|
+
const packageJson = parsePackageJson(input.packageJson);
|
|
697
735
|
const dependencies = {
|
|
698
736
|
...packageJson?.dependencies,
|
|
699
737
|
...packageJson?.devDependencies,
|
|
@@ -701,17 +739,26 @@ function packageDefaults(files, packageJsonText) {
|
|
|
701
739
|
const packageManager = packageManagerForFiles(files);
|
|
702
740
|
const run = packageManager === "npm" ? "npm run" : packageManager === "bun" ? "bun run" : packageManager;
|
|
703
741
|
const frameworkPreset = detectFrameworkPreset(files, dependencies);
|
|
742
|
+
const installCommands = [
|
|
743
|
+
installCommandForManager(packageManager, packageJson),
|
|
744
|
+
frameworkInstallCommand(frameworkPreset, files),
|
|
745
|
+
].filter((command) => Boolean(command));
|
|
746
|
+
const jekyllConfig = input.jekyllYaml
|
|
747
|
+
? parseYamlObject(input.jekyllYaml)
|
|
748
|
+
: parseTomlObject(input.jekyllToml);
|
|
704
749
|
return {
|
|
705
750
|
frameworkPreset,
|
|
706
|
-
installCommand:
|
|
707
|
-
buildCommand: buildCommandForDefaults(packageJson, run, frameworkPreset),
|
|
708
|
-
outputDirectory:
|
|
751
|
+
installCommand: installCommands.length > 0 ? installCommands.join(" && ") : null,
|
|
752
|
+
buildCommand: buildCommandForDefaults(packageJson, run, frameworkPreset, files),
|
|
753
|
+
outputDirectory: frameworkPreset === "jekyll"
|
|
754
|
+
? (stringField(jekyllConfig, "destination") ?? "_site")
|
|
755
|
+
: outputDirectoryForFramework(frameworkPreset, files),
|
|
709
756
|
};
|
|
710
757
|
}
|
|
711
758
|
export function detectBuildSettings(input) {
|
|
712
759
|
const files = new Set(input.fileNames);
|
|
713
760
|
const defaults = {
|
|
714
|
-
...packageDefaults(
|
|
761
|
+
...packageDefaults(input, files),
|
|
715
762
|
rootDirectory: input.rootDirectory ?? null,
|
|
716
763
|
platformPreset: null,
|
|
717
764
|
detectedFiles: [
|
|
@@ -721,6 +768,11 @@ export function detectBuildSettings(input) {
|
|
|
721
768
|
files.has("pnpm-lock.yaml") ? "pnpm-lock.yaml" : null,
|
|
722
769
|
files.has("yarn.lock") ? "yarn.lock" : null,
|
|
723
770
|
files.has("package-lock.json") ? "package-lock.json" : null,
|
|
771
|
+
files.has("Gemfile") ? "Gemfile" : null,
|
|
772
|
+
files.has("Gemfile.lock") ? "Gemfile.lock" : null,
|
|
773
|
+
files.has("_config.yml") ? "_config.yml" : null,
|
|
774
|
+
files.has("_config.yaml") ? "_config.yaml" : null,
|
|
775
|
+
files.has("_config.toml") ? "_config.toml" : null,
|
|
724
776
|
].filter((file) => Boolean(file)),
|
|
725
777
|
};
|
|
726
778
|
const vercelFramework = input.vercelJson
|
|
@@ -16,7 +16,7 @@ export type PolicyAuthorizeInput = {
|
|
|
16
16
|
resource: string | readonly string[];
|
|
17
17
|
conditions?: RequestCondition;
|
|
18
18
|
};
|
|
19
|
-
export declare const publishActions: ("spaces:read" | "spaces:write" | "spaces:create" | "spaces:publish" | "versions:read" | "versions:promote" | "versions:download" | "builds:read" | "builds:create")[];
|
|
19
|
+
export declare const publishActions: ("spaces:read" | "spaces:write" | "spaces:create" | "spaces:publish" | "versions:read" | "versions:promote" | "versions:download" | "builds:read" | "builds:trigger" | "builds:create")[];
|
|
20
20
|
export declare function compileApiKeyPreset(preset: ApiKeyPreset, input: PolicyCompileInput): ApiCredentialPolicy;
|
|
21
21
|
/** Expand a named API-key preset into the finite permissions sent on the public wire contract. */
|
|
22
22
|
export declare function apiKeyPresetPermissions(preset: ApiKeyPreset): StatticAction[];
|
|
@@ -8,10 +8,12 @@ export const publishActions = [
|
|
|
8
8
|
"versions:promote",
|
|
9
9
|
"versions:download",
|
|
10
10
|
"builds:read",
|
|
11
|
+
"builds:trigger",
|
|
11
12
|
"builds:create",
|
|
12
13
|
];
|
|
13
14
|
const spaceAdminActions = [
|
|
14
15
|
...publishActions,
|
|
16
|
+
"spaces:rename",
|
|
15
17
|
"spaces:delete",
|
|
16
18
|
"spaces:transfer",
|
|
17
19
|
"spaces:export",
|
|
@@ -81,12 +83,14 @@ export function compileTeamRolePolicy(input) {
|
|
|
81
83
|
"spaces:read",
|
|
82
84
|
"spaces:create",
|
|
83
85
|
"spaces:write",
|
|
86
|
+
"spaces:rename",
|
|
84
87
|
"spaces:publish",
|
|
85
88
|
"versions:read",
|
|
86
89
|
"versions:promote",
|
|
87
90
|
"versions:download",
|
|
88
91
|
"domains:read",
|
|
89
92
|
"builds:read",
|
|
93
|
+
"builds:trigger",
|
|
90
94
|
"builds:create",
|
|
91
95
|
"teams:read",
|
|
92
96
|
"billing:read",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"comment": "Shared egress-policy corpus (spec: one proxy-egress-safety invariant). Exercised by
|
|
2
|
+
"comment": "Shared egress-policy corpus (spec: one proxy-egress-safety invariant). Exercised by THREE enforcers: apps/control-plane/src/http/egress-guard.ts (control-plane SSRF guard), runtime/engine/shared/egress.php (runtime proxy egress), and crates/stattic-runtime-core (Rust finalizer policy). Every case must produce the same verdict in TypeScript, PHP, and Rust; parity is asserted by apps/control-plane/src/runtime/php-policy-parity.test.ts, src/http/egress-guard.test.ts, and crates/stattic-runtime-core/src/policy.rs::proxy_destinations_match_the_shared_egress_corpus.",
|
|
3
3
|
"ips": [
|
|
4
4
|
{ "ip": "127.0.0.1", "public": false, "reason": "loopback" },
|
|
5
5
|
{ "ip": "127.8.9.10", "public": false, "reason": "loopback /8" },
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type Grant, type GrantCapability, type GrantCheckFailure, type GrantSource } from "../contracts/grants.js";
|
|
2
|
+
export type GrantPrincipal = {
|
|
3
|
+
teamIds?: readonly string[];
|
|
4
|
+
personIds?: readonly string[];
|
|
5
|
+
linkIds?: readonly string[];
|
|
6
|
+
passwordCredentialIds?: readonly string[];
|
|
7
|
+
machineIds?: readonly string[];
|
|
8
|
+
externalIdentities?: readonly {
|
|
9
|
+
issuer: string;
|
|
10
|
+
subject: string;
|
|
11
|
+
}[];
|
|
12
|
+
verifiedEmail?: boolean;
|
|
13
|
+
ipAddress?: string;
|
|
14
|
+
country?: string;
|
|
15
|
+
userAgent?: string;
|
|
16
|
+
};
|
|
17
|
+
export type GrantEvaluationTarget = {
|
|
18
|
+
kind: "live";
|
|
19
|
+
} | {
|
|
20
|
+
kind: "version";
|
|
21
|
+
versionId: string;
|
|
22
|
+
} | {
|
|
23
|
+
kind: "branch";
|
|
24
|
+
branch: string;
|
|
25
|
+
};
|
|
26
|
+
export type GrantEvaluationInput = {
|
|
27
|
+
grants: readonly Grant[];
|
|
28
|
+
resource: string;
|
|
29
|
+
resourceKind?: "route" | "artifact";
|
|
30
|
+
target: GrantEvaluationTarget;
|
|
31
|
+
principal?: GrantPrincipal;
|
|
32
|
+
spaceClaimed: boolean;
|
|
33
|
+
now?: Date;
|
|
34
|
+
useCounts?: Readonly<Record<string, number>>;
|
|
35
|
+
acquiredGrantIds?: readonly string[];
|
|
36
|
+
};
|
|
37
|
+
export type GrantMatch = {
|
|
38
|
+
grantId: string;
|
|
39
|
+
capabilities: GrantCapability[];
|
|
40
|
+
source: GrantSource;
|
|
41
|
+
};
|
|
42
|
+
export type GrantEvaluation = {
|
|
43
|
+
capabilities: GrantCapability[];
|
|
44
|
+
matches: GrantMatch[];
|
|
45
|
+
};
|
|
46
|
+
export declare function grantResourcePatternMatches(pattern: string, resource: string, resourceKind?: "route" | "artifact"): boolean;
|
|
47
|
+
export type GrantEvaluationExplanation = {
|
|
48
|
+
grantId: string;
|
|
49
|
+
failures: GrantCheckFailure[];
|
|
50
|
+
};
|
|
51
|
+
export declare function explainGrants(input: GrantEvaluationInput): GrantEvaluationExplanation[];
|
|
52
|
+
export declare function evaluateGrants(input: GrantEvaluationInput): GrantEvaluation;
|
|
53
|
+
export declare function grantAllows(input: GrantEvaluationInput, capability: GrantCapability): boolean;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { normalizeAccessScopePath } from "../contracts/access.js";
|
|
2
|
+
import { GRANT_CAPABILITIES, grantNetworkCidrMatches, normalizeGrantResourcePattern, } from "../contracts/grants.js";
|
|
3
|
+
function audienceMatches(audience, principal) {
|
|
4
|
+
switch (audience.kind) {
|
|
5
|
+
case "public":
|
|
6
|
+
return true;
|
|
7
|
+
case "team":
|
|
8
|
+
return principal.teamIds?.includes(audience.teamId) ?? false;
|
|
9
|
+
case "person":
|
|
10
|
+
return principal.personIds?.includes(audience.personId) ?? false;
|
|
11
|
+
case "link":
|
|
12
|
+
return principal.linkIds?.includes(audience.linkId) ?? false;
|
|
13
|
+
case "password":
|
|
14
|
+
return principal.passwordCredentialIds?.includes(audience.credentialId) ?? false;
|
|
15
|
+
case "machine":
|
|
16
|
+
return principal.machineIds?.includes(audience.machineId) ?? false;
|
|
17
|
+
case "external":
|
|
18
|
+
return (principal.externalIdentities?.some((identity) => identity.issuer === audience.issuer && identity.subject === audience.subject) ?? false);
|
|
19
|
+
}
|
|
20
|
+
throw new Error("Unsupported Grant audience.");
|
|
21
|
+
}
|
|
22
|
+
function targetMatches(selector, target) {
|
|
23
|
+
switch (selector.kind) {
|
|
24
|
+
case "live":
|
|
25
|
+
return target.kind === "live";
|
|
26
|
+
case "version":
|
|
27
|
+
return target.kind === "version" && target.versionId === selector.versionId;
|
|
28
|
+
case "branch":
|
|
29
|
+
return target.kind === "branch" && target.branch === selector.branch;
|
|
30
|
+
case "all_versions":
|
|
31
|
+
return target.kind === "version";
|
|
32
|
+
}
|
|
33
|
+
throw new Error("Unsupported Grant target.");
|
|
34
|
+
}
|
|
35
|
+
function normalizeGrantEvaluationResource(resource, resourceKind) {
|
|
36
|
+
if (resourceKind === "route")
|
|
37
|
+
return normalizeAccessScopePath(resource);
|
|
38
|
+
if (!resource.startsWith("/")) {
|
|
39
|
+
throw new TypeError("artifact resource must be an absolute path");
|
|
40
|
+
}
|
|
41
|
+
const canonical = normalizeGrantResourcePattern(resource);
|
|
42
|
+
if (canonical.includes("*")) {
|
|
43
|
+
throw new TypeError("artifact resource cannot contain wildcards");
|
|
44
|
+
}
|
|
45
|
+
return canonical;
|
|
46
|
+
}
|
|
47
|
+
export function grantResourcePatternMatches(pattern, resource, resourceKind = "route") {
|
|
48
|
+
const canonicalPattern = normalizeGrantResourcePattern(pattern);
|
|
49
|
+
const canonicalResource = normalizeGrantEvaluationResource(resource, resourceKind);
|
|
50
|
+
const patternSegments = canonicalPattern === "/" ? [] : canonicalPattern.slice(1).split("/");
|
|
51
|
+
const resourceSegments = canonicalResource === "/" ? [] : canonicalResource.slice(1).split("/");
|
|
52
|
+
const matches = (patternIndex, resourceIndex) => {
|
|
53
|
+
const patternSegment = patternSegments[patternIndex];
|
|
54
|
+
if (patternSegment === undefined)
|
|
55
|
+
return resourceIndex === resourceSegments.length;
|
|
56
|
+
if (patternSegment === "**") {
|
|
57
|
+
if (patternIndex === patternSegments.length - 1)
|
|
58
|
+
return true;
|
|
59
|
+
for (let next = resourceIndex; next <= resourceSegments.length; next++) {
|
|
60
|
+
if (matches(patternIndex + 1, next))
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
if (resourceIndex >= resourceSegments.length)
|
|
66
|
+
return false;
|
|
67
|
+
if (patternSegment !== "*" && patternSegment !== resourceSegments[resourceIndex])
|
|
68
|
+
return false;
|
|
69
|
+
return matches(patternIndex + 1, resourceIndex + 1);
|
|
70
|
+
};
|
|
71
|
+
return matches(0, 0);
|
|
72
|
+
}
|
|
73
|
+
function grantCheckFailures(grant, input, principal, now) {
|
|
74
|
+
const failures = [];
|
|
75
|
+
if (grant.source.kind === "config" && !input.spaceClaimed) {
|
|
76
|
+
failures.push("space_unclaimed_config");
|
|
77
|
+
}
|
|
78
|
+
if (!audienceMatches(grant.audience, principal))
|
|
79
|
+
failures.push("audience");
|
|
80
|
+
if (!targetMatches(grant.target, input.target))
|
|
81
|
+
failures.push("target");
|
|
82
|
+
const timestamp = now.getTime();
|
|
83
|
+
if (!Number.isFinite(timestamp)) {
|
|
84
|
+
failures.push("invalid_time");
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
const notBefore = grant.constraints.notBefore;
|
|
88
|
+
if (notBefore !== undefined && timestamp < Date.parse(notBefore))
|
|
89
|
+
failures.push("not_before");
|
|
90
|
+
const expiresAt = grant.constraints.expiresAt;
|
|
91
|
+
if (expiresAt !== undefined && timestamp >= Date.parse(expiresAt))
|
|
92
|
+
failures.push("expired");
|
|
93
|
+
}
|
|
94
|
+
const maxUses = grant.constraints.maxUses;
|
|
95
|
+
if (maxUses !== undefined && !input.acquiredGrantIds?.includes(grant.id)) {
|
|
96
|
+
const uses = input.useCounts?.[grant.id];
|
|
97
|
+
if (!Number.isInteger(uses) || (uses ?? 0) < 0 || (uses ?? 0) >= maxUses) {
|
|
98
|
+
failures.push("max_uses");
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (grant.constraints.requireVerifiedEmail && principal.verifiedEmail !== true) {
|
|
102
|
+
failures.push("verified_email");
|
|
103
|
+
}
|
|
104
|
+
if (grant.constraints.network &&
|
|
105
|
+
((grant.constraints.network.ipCidrs !== undefined &&
|
|
106
|
+
(!principal.ipAddress ||
|
|
107
|
+
!grant.constraints.network.ipCidrs.some((cidr) => grantNetworkCidrMatches(cidr, principal.ipAddress ?? "")))) ||
|
|
108
|
+
(grant.constraints.network.countries !== undefined &&
|
|
109
|
+
(!principal.country ||
|
|
110
|
+
!grant.constraints.network.countries.includes(principal.country.toUpperCase()))) ||
|
|
111
|
+
(grant.constraints.network.excludedCountries !== undefined &&
|
|
112
|
+
(!principal.country ||
|
|
113
|
+
grant.constraints.network.excludedCountries.includes(principal.country.toUpperCase()))) ||
|
|
114
|
+
(grant.constraints.network.excludedUserAgentSubstrings !== undefined &&
|
|
115
|
+
(!principal.userAgent ||
|
|
116
|
+
grant.constraints.network.excludedUserAgentSubstrings.some((needle) => principal.userAgent?.toLowerCase().includes(needle.toLowerCase())))))) {
|
|
117
|
+
failures.push("network");
|
|
118
|
+
}
|
|
119
|
+
const included = grant.resources.include.some((pattern) => grantResourcePatternMatches(pattern, input.resource, input.resourceKind));
|
|
120
|
+
if (!included) {
|
|
121
|
+
failures.push("resource_not_included");
|
|
122
|
+
}
|
|
123
|
+
else if (grant.resources.exclude.some((pattern) => grantResourcePatternMatches(pattern, input.resource, input.resourceKind))) {
|
|
124
|
+
failures.push("resource_excluded");
|
|
125
|
+
}
|
|
126
|
+
return failures;
|
|
127
|
+
}
|
|
128
|
+
export function explainGrants(input) {
|
|
129
|
+
const principal = input.principal ?? {};
|
|
130
|
+
const now = input.now ?? new Date();
|
|
131
|
+
return input.grants.map((grant) => ({
|
|
132
|
+
grantId: grant.id,
|
|
133
|
+
failures: grantCheckFailures(grant, input, principal, now),
|
|
134
|
+
}));
|
|
135
|
+
}
|
|
136
|
+
export function evaluateGrants(input) {
|
|
137
|
+
const principal = input.principal ?? {};
|
|
138
|
+
const now = input.now ?? new Date();
|
|
139
|
+
const matches = [];
|
|
140
|
+
const capabilities = new Set();
|
|
141
|
+
for (const grant of input.grants) {
|
|
142
|
+
if (grantCheckFailures(grant, input, principal, now).length > 0)
|
|
143
|
+
continue;
|
|
144
|
+
const matchedCapabilities = [...grant.capabilities];
|
|
145
|
+
matches.push({
|
|
146
|
+
grantId: grant.id,
|
|
147
|
+
capabilities: matchedCapabilities,
|
|
148
|
+
source: grant.source,
|
|
149
|
+
});
|
|
150
|
+
for (const capability of matchedCapabilities)
|
|
151
|
+
capabilities.add(capability);
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
capabilities: GRANT_CAPABILITIES.filter((capability) => capabilities.has(capability)),
|
|
155
|
+
matches,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
export function grantAllows(input, capability) {
|
|
159
|
+
return evaluateGrants(input).capabilities.includes(capability);
|
|
160
|
+
}
|
|
@@ -131,6 +131,7 @@ export async function findUpward(probe, startDir = process.cwd(), boundary) {
|
|
|
131
131
|
current = resolvedBoundary;
|
|
132
132
|
}
|
|
133
133
|
for (;;) {
|
|
134
|
+
// oxlint-disable-next-line no-await-in-loop -- each parent probe depends on the child being absent
|
|
134
135
|
const found = await probe(current);
|
|
135
136
|
if (found)
|
|
136
137
|
return found;
|
package/dist/utils/pages.d.ts
CHANGED
|
@@ -16,17 +16,23 @@ export type PageFileEntry = {
|
|
|
16
16
|
export type PagePreviewFile = {
|
|
17
17
|
name: string;
|
|
18
18
|
href: string;
|
|
19
|
-
downloadHref?: string;
|
|
20
19
|
size?: string;
|
|
21
20
|
mime?: string;
|
|
22
21
|
};
|
|
22
|
+
/**
|
|
23
|
+
* One URL, one representation. A file's bare path always serves its bytes;
|
|
24
|
+
* the preview page is reachable only under this query flag. The alternative —
|
|
25
|
+
* negotiating on `Accept` at the bare path — depends on `Vary`, which the edge
|
|
26
|
+
* ignores, so whichever client arrived first pinned that representation for
|
|
27
|
+
* everyone (an `<img>` could poison the human's link for a year).
|
|
28
|
+
*/
|
|
29
|
+
export declare const PAGE_PREVIEW_QUERY = "spacefast_view";
|
|
30
|
+
/** The preview page for a file whose bytes live at `fileHref`. */
|
|
31
|
+
export declare function previewPageHref(fileHref: string): string;
|
|
23
32
|
export type PageExpansionContext = {
|
|
24
33
|
page: SitePageId | "layout";
|
|
25
34
|
theme?: PageTheme;
|
|
26
35
|
content?: string;
|
|
27
|
-
passwordAction?: string;
|
|
28
|
-
passwordError?: string;
|
|
29
|
-
loginHref?: string;
|
|
30
36
|
displayPath?: string;
|
|
31
37
|
displayTitle?: string;
|
|
32
38
|
itemCount?: number;
|
|
@@ -38,7 +44,7 @@ export type PageExpansionContext = {
|
|
|
38
44
|
};
|
|
39
45
|
export type PageDiagnostic = {
|
|
40
46
|
severity: "warning" | "error";
|
|
41
|
-
code: "page_source_too_large" | "unknown_page_template" | "unknown_page_element" | "page_element_not_allowed" | "page_document_required" | "layout_missing_content" | "
|
|
47
|
+
code: "page_source_too_large" | "unknown_page_template" | "unknown_page_element" | "page_element_not_allowed" | "page_document_required" | "layout_missing_content" | "index_files_missing" | "page_indexable";
|
|
42
48
|
message: string;
|
|
43
49
|
path: string;
|
|
44
50
|
};
|
package/dist/utils/pages.js
CHANGED
|
@@ -3,6 +3,18 @@ import { CUSTOM_PAGE_IDS, PAGE_CATALOG, PAGE_ELEMENTS, PAGE_LAYOUT_FILE, PAGE_SO
|
|
|
3
3
|
import { SPACE_CONFIG_ACCEPTED_FILES } from "../contracts/space-config.js";
|
|
4
4
|
import { derivePageColors, PAGE_COLOR_DEFAULTS } from "./page-colors.js";
|
|
5
5
|
import { PAGE_EMBEDDED_FONT_FACES, PAGE_FONT_FACES } from "./page-fonts.js";
|
|
6
|
+
/**
|
|
7
|
+
* One URL, one representation. A file's bare path always serves its bytes;
|
|
8
|
+
* the preview page is reachable only under this query flag. The alternative —
|
|
9
|
+
* negotiating on `Accept` at the bare path — depends on `Vary`, which the edge
|
|
10
|
+
* ignores, so whichever client arrived first pinned that representation for
|
|
11
|
+
* everyone (an `<img>` could poison the human's link for a year).
|
|
12
|
+
*/
|
|
13
|
+
export const PAGE_PREVIEW_QUERY = "spacefast_view";
|
|
14
|
+
/** The preview page for a file whose bytes live at `fileHref`. */
|
|
15
|
+
export function previewPageHref(fileHref) {
|
|
16
|
+
return `${fileHref}${fileHref.includes("?") ? "&" : "?"}${PAGE_PREVIEW_QUERY}=1`;
|
|
17
|
+
}
|
|
6
18
|
const encoder = new TextEncoder();
|
|
7
19
|
const knownElements = new Set(PAGE_ELEMENTS);
|
|
8
20
|
const customPageIds = new Set(CUSTOM_PAGE_IDS);
|
|
@@ -69,16 +81,14 @@ function renderFiles(files) {
|
|
|
69
81
|
function renderFile(file) {
|
|
70
82
|
if (!file)
|
|
71
83
|
return '<p class="sf-empty">File unavailable.</p>';
|
|
72
|
-
|
|
73
|
-
|
|
84
|
+
// This page is itself the `?spacefast_view=1` representation, so the embed,
|
|
85
|
+
// the download and the raw link all point straight at the canonical path.
|
|
86
|
+
const href = escapePageHtml(file.href);
|
|
74
87
|
const name = escapePageHtml(file.name);
|
|
75
88
|
const media = file.mime?.startsWith("image/")
|
|
76
89
|
? `<img class="sf-preview" src="${href}" alt="">`
|
|
77
90
|
: `<iframe class="sf-preview" src="${href}" title="Preview of ${name}" loading="lazy" sandbox></iframe>`;
|
|
78
|
-
|
|
79
|
-
? `${file.downloadHref}${file.downloadHref.includes("?") ? "&" : "?"}spacefast_raw=1`
|
|
80
|
-
: rawHref;
|
|
81
|
-
return `<div class="sf-preview-wrap">${media}</div><dl class="sf-file-meta"><div><dt>Name</dt><dd>${name}</dd></div>${file.size ? `<div><dt>Size</dt><dd>${escapePageHtml(file.size)}</dd></div>` : ""}${file.mime ? `<div><dt>Type</dt><dd>${escapePageHtml(file.mime)}</dd></div>` : ""}</dl><div class="sf-actions"><a class="sf-button" href="${escapePageHtml(downloadHref)}" download>Download</a><a class="sf-ghost" href="${href}">View raw</a></div>`;
|
|
91
|
+
return `<div class="sf-preview-wrap">${media}</div><dl class="sf-file-meta"><div><dt>Name</dt><dd>${name}</dd></div>${file.size ? `<div><dt>Size</dt><dd>${escapePageHtml(file.size)}</dd></div>` : ""}${file.mime ? `<div><dt>Type</dt><dd>${escapePageHtml(file.mime)}</dd></div>` : ""}</dl><div class="sf-actions"><a class="sf-button" href="${href}" download>Download</a><a class="sf-ghost" href="${href}">View raw</a></div>`;
|
|
82
92
|
}
|
|
83
93
|
function renderSpacefastBranding(hidden) {
|
|
84
94
|
const wordmark = hidden
|
|
@@ -90,18 +100,8 @@ function renderElement(element, context) {
|
|
|
90
100
|
switch (element) {
|
|
91
101
|
case "sf-content":
|
|
92
102
|
return context.content ?? "";
|
|
93
|
-
case "sf-password-form": {
|
|
94
|
-
const hasError = Boolean(context.passwordError);
|
|
95
|
-
const error = hasError
|
|
96
|
-
? `<p class="sf-error" id="sf-password-error" role="alert">${escapePageHtml(context.passwordError ?? "")}</p>`
|
|
97
|
-
: "";
|
|
98
|
-
const form = `<form class="sf-form" method="post" action="${escapePageHtml(context.passwordAction ?? "")}"><label for="sf-password">Password</label><div class="sf-form-row"><input id="sf-password" name="_pw" type="password" required autocomplete="current-password"${hasError ? ' aria-invalid="true" aria-describedby="sf-password-error"' : ""}><button type="submit">Continue</button></div>${error}</form>`;
|
|
99
|
-
return runtimeSlot("challenge", form);
|
|
100
|
-
}
|
|
101
|
-
case "sf-login":
|
|
102
|
-
return runtimeSlot("challenge", `<div class="sf-actions"><a class="sf-button" href="${escapePageHtml(context.loginHref ?? "/__spacefast/access/login")}">Continue with Spacefast</a></div>`);
|
|
103
103
|
case "sf-denial-context":
|
|
104
|
-
return runtimeSlot("denial", '<p class="sf-copy">
|
|
104
|
+
return runtimeSlot("denial", '<p class="sf-copy">This page is private.</p>');
|
|
105
105
|
case "sf-page-path":
|
|
106
106
|
return context.displayPath
|
|
107
107
|
? escapePageHtml(context.displayPath)
|
|
@@ -192,7 +192,6 @@ export function validatePageSources(input) {
|
|
|
192
192
|
element === "sf-name" ||
|
|
193
193
|
element === "sf-spacefast-branding" ||
|
|
194
194
|
(id === "404" && element === "sf-page-path") ||
|
|
195
|
-
(id === "password" && element === "sf-password-form") ||
|
|
196
195
|
(id === "denied" && element === "sf-denial-context") ||
|
|
197
196
|
(id === "index" &&
|
|
198
197
|
(element === "sf-files" ||
|
|
@@ -209,16 +208,6 @@ export function validatePageSources(input) {
|
|
|
209
208
|
path,
|
|
210
209
|
});
|
|
211
210
|
}
|
|
212
|
-
if (id === "password" &&
|
|
213
|
-
!/<\s*sf-password-form\b/i.test(source) &&
|
|
214
|
-
!/<\s*input\b[^>]*\bname\s*=\s*["']_pw["']/i.test(source)) {
|
|
215
|
-
diagnostics.push({
|
|
216
|
-
severity: "error",
|
|
217
|
-
code: "password_form_missing",
|
|
218
|
-
message: "The password page needs <sf-password-form> or a form input named _pw.",
|
|
219
|
-
path,
|
|
220
|
-
});
|
|
221
|
-
}
|
|
222
211
|
if (id === "index" && !/<\s*sf-files\b/i.test(source))
|
|
223
212
|
diagnostics.push({
|
|
224
213
|
severity: "warning",
|
|
@@ -226,7 +215,7 @@ export function validatePageSources(input) {
|
|
|
226
215
|
message: "The index page does not render <sf-files>.",
|
|
227
216
|
path,
|
|
228
217
|
});
|
|
229
|
-
if (
|
|
218
|
+
if (id === "denied" &&
|
|
230
219
|
/<\s*meta\b[^>]*\bname\s*=\s*["']robots["'][^>]*\bcontent\s*=\s*["'][^"']*\bindex\b/i.test(source))
|
|
231
220
|
diagnostics.push({
|
|
232
221
|
severity: "warning",
|
|
@@ -332,7 +321,7 @@ export function defaultPageBody(id) {
|
|
|
332
321
|
// The built-in palette runs through the same readability engine as pushed
|
|
333
322
|
// themes, so buttons and links remain readable for every accepted accent.
|
|
334
323
|
const defaultPageColors = derivePageColors({});
|
|
335
|
-
const sharedCss = `/*sf-default-fonts*/:root{color-scheme:light dark;--sf-accent:${PAGE_COLOR_DEFAULTS.accent};--sf-bg:${PAGE_COLOR_DEFAULTS.bg};--sf-fg:${PAGE_COLOR_DEFAULTS.fg};--sf-muted:${PAGE_COLOR_DEFAULTS.muted};--sf-card:${PAGE_COLOR_DEFAULTS.card};--sf-border:${PAGE_COLOR_DEFAULTS.border};--sf-link:${defaultPageColors.link};--sf-on-accent:${defaultPageColors.onAccent};--sf-error:#a82d1a;--sf-font:"Haskoy",-apple-system,BlinkMacSystemFont,ui-sans-serif,"Segoe UI",sans-serif;--sf-display-font:"Merriweather",Georgia,serif;--sf-mono:ui-monospace,"SF Mono",Menlo,Consolas,monospace}*{box-sizing:border-box}html{min-width:320px;background:var(--sf-bg);-webkit-font-smoothing:antialiased}body{margin:0;min-height:100vh;background:var(--sf-bg);color:var(--sf-fg);font:14px/1.65 var(--sf-font)}a{color:inherit}.sf-page{min-height:100vh;display:flex;flex-direction:column;padding:clamp(22px,4vw,44px)}.sf-header{display:flex;align-items:center;min-height:32px}.sf-page-center .sf-header{justify-content:center}.sf-brand{display:inline-flex;min-width:0;align-items:center;gap:10px;color:var(--sf-fg);text-decoration:none}.sf-brand-mark{width:7px;height:7px;flex:none;background:var(--sf-accent)}.sf-logo{width:28px;height:28px;flex:none;object-fit:contain}.sf-brand strong{min-width:0;overflow-wrap:anywhere;font-size:13px;font-weight:600}.sf-main{flex:1;width:100%;padding:clamp(34px,9vh,84px) 0}.sf-page-plain .sf-main{max-width:680px}.sf-page-center .sf-main{max-width:620px;margin:auto;display:flex;flex-direction:column;align-items:center;text-align:center}.sf-eyebrow{margin:0 0 14px;color:var(--sf-muted);font:500 11px/1.5 var(--sf-mono);letter-spacing:.12em;text-transform:uppercase;overflow-wrap:anywhere}.sf-page-center .sf-eyebrow{margin-bottom:16px}h1{max-width:20ch;margin:0 0 12px;color:var(--sf-fg);font:430 clamp(1.9rem,5vw,2.8rem)/1.22 var(--sf-display-font);letter-spacing:-.012em;text-wrap:balance}.sf-copy,.sf-sub{max-width:52ch;margin:0;color:var(--sf-muted);text-wrap:pretty}.sf-inline-path{font-family:var(--sf-mono);font-size:.9em;color:var(--sf-fg);overflow-wrap:anywhere}.sf-actions{display:flex;flex-wrap:wrap;align-items:center;gap:14px;margin-top:22px}.sf-page-center .sf-actions{justify-content:center}.sf-button
|
|
324
|
+
const sharedCss = `/*sf-default-fonts*/:root{color-scheme:light dark;--sf-accent:${PAGE_COLOR_DEFAULTS.accent};--sf-bg:${PAGE_COLOR_DEFAULTS.bg};--sf-fg:${PAGE_COLOR_DEFAULTS.fg};--sf-muted:${PAGE_COLOR_DEFAULTS.muted};--sf-card:${PAGE_COLOR_DEFAULTS.card};--sf-border:${PAGE_COLOR_DEFAULTS.border};--sf-link:${defaultPageColors.link};--sf-on-accent:${defaultPageColors.onAccent};--sf-error:#a82d1a;--sf-font:"Haskoy",-apple-system,BlinkMacSystemFont,ui-sans-serif,"Segoe UI",sans-serif;--sf-display-font:"Merriweather",Georgia,serif;--sf-mono:ui-monospace,"SF Mono",Menlo,Consolas,monospace}*{box-sizing:border-box}html{min-width:320px;background:var(--sf-bg);-webkit-font-smoothing:antialiased}body{margin:0;min-height:100vh;background:var(--sf-bg);color:var(--sf-fg);font:14px/1.65 var(--sf-font)}a{color:inherit}.sf-page{min-height:100vh;display:flex;flex-direction:column;padding:clamp(22px,4vw,44px)}.sf-header{display:flex;align-items:center;min-height:32px}.sf-page-center .sf-header{justify-content:center}.sf-brand{display:inline-flex;min-width:0;align-items:center;gap:10px;color:var(--sf-fg);text-decoration:none}.sf-brand-mark{width:7px;height:7px;flex:none;background:var(--sf-accent)}.sf-logo{width:28px;height:28px;flex:none;object-fit:contain}.sf-brand strong{min-width:0;overflow-wrap:anywhere;font-size:13px;font-weight:600}.sf-main{flex:1;width:100%;padding:clamp(34px,9vh,84px) 0}.sf-page-plain .sf-main{max-width:680px}.sf-page-center .sf-main{max-width:620px;margin:auto;display:flex;flex-direction:column;align-items:center;text-align:center}.sf-eyebrow{margin:0 0 14px;color:var(--sf-muted);font:500 11px/1.5 var(--sf-mono);letter-spacing:.12em;text-transform:uppercase;overflow-wrap:anywhere}.sf-page-center .sf-eyebrow{margin-bottom:16px}h1{max-width:20ch;margin:0 0 12px;color:var(--sf-fg);font:430 clamp(1.9rem,5vw,2.8rem)/1.22 var(--sf-display-font);letter-spacing:-.012em;text-wrap:balance}.sf-copy,.sf-sub{max-width:52ch;margin:0;color:var(--sf-muted);text-wrap:pretty}.sf-inline-path{font-family:var(--sf-mono);font-size:.9em;color:var(--sf-fg);overflow-wrap:anywhere}.sf-actions{display:flex;flex-wrap:wrap;align-items:center;gap:14px;margin-top:22px}.sf-page-center .sf-actions{justify-content:center}.sf-button{display:inline-flex;min-height:44px;align-items:center;justify-content:center;border:0;background:var(--sf-accent);color:var(--sf-on-accent);padding:10px 16px;font:600 13px/1.4 var(--sf-font);text-decoration:none;cursor:pointer}.sf-ghost{color:var(--sf-muted);text-underline-offset:3px}.sf-button:hover{filter:brightness(1.06)}a:focus-visible,button:focus-visible{outline:2px solid var(--sf-link);outline-offset:2px}.sf-copy+.sf-copy{margin-top:12px}.sf-files{width:100%;margin:24px 0 0;padding:0;list-style:none}.sf-files li{display:flex;min-width:0;align-items:baseline;gap:18px;border-bottom:1px solid var(--sf-border)}.sf-files a{min-width:0;flex:1;padding:10px 2px;color:var(--sf-fg);font-weight:500;text-decoration:none}.sf-files a:hover{color:var(--sf-link)}.sf-file-name{overflow-wrap:anywhere}.sf-file-size{flex:none;padding:10px 2px;color:var(--sf-muted);font:12px/1.5 var(--sf-mono);white-space:nowrap}.sf-empty{max-width:420px;margin:24px 0 0;border:1px dashed var(--sf-border);padding:18px 20px;color:var(--sf-muted)}.sf-preview-wrap{width:min(100%,560px);aspect-ratio:16/9;margin:10px 0 18px;overflow:hidden;background:#fff;outline:1px solid color-mix(in srgb,var(--sf-fg) 10%,transparent);outline-offset:-1px}.sf-preview{display:block;width:100%;height:100%;border:0;object-fit:contain}.sf-file-meta{width:min(100%,560px);margin:0}.sf-file-meta div{display:flex;min-width:0;justify-content:space-between;gap:20px;border-bottom:1px solid var(--sf-border);padding:9px 2px}.sf-file-meta dt{color:var(--sf-fg);font-weight:600}.sf-file-meta dd{min-width:0;margin:0;color:var(--sf-muted);font-family:var(--sf-mono);overflow-wrap:anywhere;text-align:right}.sf-colophon{display:flex;flex-direction:column;align-items:center;gap:10px;padding:44px 0 26px;margin-top:auto;color:var(--sf-muted)}.sf-page-plain .sf-colophon{align-items:flex-start}.sf-colophon-row{display:flex;align-items:center;gap:20px;flex-wrap:wrap}.sf-powered{display:inline-flex;align-items:center;gap:10px;cursor:pointer;outline:none}.sf-powered-line{display:none;font-size:12px;color:var(--sf-muted)}.sf-powered:hover .sf-powered-line,.sf-powered:focus .sf-powered-line,.sf-powered:focus-within .sf-powered-line{display:inline}.sf-powered-line a{color:var(--sf-link);text-decoration:underline;text-underline-offset:3px}.sf-help{font-size:12px;color:var(--sf-muted);text-decoration:underline;text-underline-offset:3px}.sf-help:hover{color:var(--sf-fg)}.sf-wordmark{display:block;flex:none;width:auto;height:11px}.sf-wordmark-shadow{fill:var(--sf-accent)}.sf-wordmark-plate{fill:#141419}.sf-wordmark-paper{fill:#fbf8f1}@media(max-width:520px){.sf-page{padding:20px}.sf-button{min-height:48px}.sf-file-meta div{flex-direction:column;gap:2px}.sf-file-meta dd{text-align:left}.sf-colophon{padding-top:32px}}@media(prefers-color-scheme:dark){:root{--sf-bg:${PAGE_COLOR_DEFAULTS.darkBg};--sf-fg:${PAGE_COLOR_DEFAULTS.darkFg};--sf-muted:${PAGE_COLOR_DEFAULTS.darkMuted};--sf-card:${PAGE_COLOR_DEFAULTS.darkCard};--sf-border:${PAGE_COLOR_DEFAULTS.darkBorder};--sf-link:${defaultPageColors.linkDark};--sf-error:#ff8a7a}.sf-preview-wrap{outline-color:#ffffff1a}}@media(forced-colors:active){.sf-brand-mark{background:CanvasText}.sf-button{border:1px solid ButtonText}.sf-powered:focus{outline:1px solid CanvasText;outline-offset:3px}}`;
|
|
336
325
|
const pageHeader = '<header class="sf-header"><a class="sf-brand" href="/" aria-label="Homepage"><span class="sf-brand-mark" aria-hidden="true"></span><sf-logo></sf-logo><strong><sf-name></sf-name></strong></a></header>';
|
|
337
326
|
const pageFooter = "<sf-spacefast-branding></sf-spacefast-branding>";
|
|
338
327
|
function document(id, title, body, layout) {
|
|
@@ -341,9 +330,7 @@ function document(id, title, body, layout) {
|
|
|
341
330
|
}
|
|
342
331
|
export const DEFAULT_PAGE_TEMPLATES = {
|
|
343
332
|
"404": document("404", "Page not found", '<p class="sf-eyebrow">404 · Not Found</p><h1>Page not found</h1><p class="sf-copy">There’s nothing at <span class="sf-inline-path"><sf-page-path></sf-page-path></span>.</p><div class="sf-actions"><a class="sf-button" href="/">Go to the homepage</a></div>', "center"),
|
|
344
|
-
password: document("password", "Password required", '<h1>This space is protected</h1><p class="sf-sub">Enter the password to continue.</p><sf-password-form></sf-password-form>', "center"),
|
|
345
333
|
denied: document("denied", "Access denied", "<h1>Access denied</h1><sf-denial-context></sf-denial-context>", "center"),
|
|
346
|
-
login: document("login", "Sign in", '<h1>This space is for members</h1><p class="sf-sub">Sign in to continue — you’ll come right back here.</p><sf-login></sf-login>', "center"),
|
|
347
334
|
index: document("index", "<sf-page-title></sf-page-title>", '<p class="sf-eyebrow"><sf-page-path></sf-page-path></p><h1><sf-page-title></sf-page-title></h1><p class="sf-sub"><sf-page-summary></sf-page-summary></p><sf-files></sf-files>', "plain"),
|
|
348
335
|
preview: document("preview", "<sf-page-title></sf-page-title>", '<p class="sf-eyebrow"><sf-page-path></sf-page-path></p><h1><sf-page-title></sf-page-title></h1><sf-file></sf-file>', "plain"),
|
|
349
336
|
};
|
|
@@ -46,6 +46,40 @@ export type PublishManifestResult<T extends {
|
|
|
46
46
|
files: T[];
|
|
47
47
|
warnings: PublishManifestWarning[];
|
|
48
48
|
};
|
|
49
|
+
export type PublishRouteAnalysis = {
|
|
50
|
+
collisions: Array<{
|
|
51
|
+
route: string;
|
|
52
|
+
paths: string[];
|
|
53
|
+
}>;
|
|
54
|
+
caseCollisions: Array<{
|
|
55
|
+
route: string;
|
|
56
|
+
siblingRoute: string;
|
|
57
|
+
paths: string[];
|
|
58
|
+
}>;
|
|
59
|
+
routeOwnership: PublishRouteOwnership[];
|
|
60
|
+
};
|
|
61
|
+
export type PublishRouteOwnership = {
|
|
62
|
+
requestedRoute: string;
|
|
63
|
+
artifactPath: string;
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Canonicalizes a route expanded from an already-canonical manifest path.
|
|
67
|
+
* Manifest intake has decoded percent escapes exactly once, so this must not
|
|
68
|
+
* decode again. It applies only the route-identity aliases shared with access.
|
|
69
|
+
*/
|
|
70
|
+
export declare function canonicalizePublishRequestedRoute(route: string): string;
|
|
71
|
+
/**
|
|
72
|
+
* Expands artifact paths into the request routes they can serve. The runtime
|
|
73
|
+
* resolves literal files, directory indexes, and clean `.html` aliases; two
|
|
74
|
+
* different artifacts owning one request route is rejected at publish/finalize
|
|
75
|
+
* instead of depending on lookup order. `routeOwnership` exposes the same
|
|
76
|
+
* expansion in canonical access-route form so authorization and diagnostics
|
|
77
|
+
* never have to infer a route from an artifact path.
|
|
78
|
+
*/
|
|
79
|
+
export declare function analyzePublishRoutes(paths: readonly string[], options?: {
|
|
80
|
+
index?: string | false;
|
|
81
|
+
cleanUrls?: boolean;
|
|
82
|
+
}): PublishRouteAnalysis;
|
|
49
83
|
export declare function resolveManifestFileCap(maxFiles?: number | null): number;
|
|
50
84
|
export declare function validatePublishManifest<T extends {
|
|
51
85
|
path: string;
|
|
@@ -64,7 +98,3 @@ export declare function filterFreePlanManifest<T extends {
|
|
|
64
98
|
allowed: T[];
|
|
65
99
|
ignored: string[];
|
|
66
100
|
};
|
|
67
|
-
export declare function filterFreePlanHeaders(content: string): {
|
|
68
|
-
headers: string | undefined;
|
|
69
|
-
droppedBasicAuthCount: number;
|
|
70
|
-
};
|