@spacefast/common 0.0.12 → 0.0.17
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/agents/client-registry.d.ts +22 -0
- package/dist/agents/client-registry.js +41 -0
- package/dist/agents/connect-targets.d.ts +51 -6
- package/dist/agents/connect-targets.js +93 -28
- package/dist/brand.d.ts +0 -11
- package/dist/brand.js +0 -20
- package/dist/config/domains.d.ts +15 -6
- package/dist/config/domains.js +24 -10
- package/dist/config/postgres-budget.d.ts +3 -0
- package/dist/config/postgres-budget.js +6 -1
- package/dist/config/postgres-budget.json +2 -1
- package/dist/contracts/abuse.js +1 -1
- package/dist/contracts/access.d.ts +486 -1196
- package/dist/contracts/access.js +323 -1054
- package/dist/contracts/activity.d.ts +1 -1
- package/dist/contracts/activity.js +11 -15
- package/dist/contracts/api-keys.d.ts +16 -0
- package/dist/contracts/api-keys.js +11 -9
- package/dist/contracts/archives.d.ts +1 -2
- package/dist/contracts/archives.js +15 -17
- package/dist/contracts/beta.js +1 -1
- package/dist/contracts/builds.js +14 -14
- package/dist/contracts/channels.js +2 -2
- package/dist/contracts/comments.d.ts +143 -99
- package/dist/contracts/comments.js +194 -79
- package/dist/contracts/common.d.ts +12 -12
- package/dist/contracts/common.js +33 -30
- package/dist/contracts/continuation.js +5 -5
- 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 +13 -9
- package/dist/contracts/device-auth.js +12 -12
- package/dist/contracts/docs.d.ts +10 -10
- package/dist/contracts/domains.js +39 -33
- package/dist/contracts/enums.d.ts +4 -14
- package/dist/contracts/enums.js +3 -10
- package/dist/contracts/error-code-meta.d.ts +122 -56
- package/dist/contracts/error-code-meta.js +43 -19
- package/dist/contracts/error-codes.d.ts +7 -4
- package/dist/contracts/error-codes.js +48 -24
- package/dist/contracts/events.d.ts +22 -15
- package/dist/contracts/events.js +33 -5
- package/dist/contracts/execution.d.ts +238 -0
- package/dist/contracts/execution.js +309 -0
- package/dist/contracts/feature-lifecycle.d.ts +39 -188
- package/dist/contracts/feature-lifecycle.js +52 -196
- package/dist/contracts/features.d.ts +13 -115
- package/dist/contracts/features.js +5 -35
- package/dist/contracts/functions.d.ts +597 -0
- package/dist/contracts/functions.js +475 -0
- package/dist/contracts/grants.d.ts +1175 -0
- package/dist/contracts/grants.js +783 -0
- package/dist/contracts/ids.d.ts +7 -5
- package/dist/contracts/ids.js +7 -7
- package/dist/contracts/internal.js +2 -2
- package/dist/contracts/mcp.d.ts +17 -556
- package/dist/contracts/mcp.js +9 -103
- package/dist/contracts/me.js +3 -4
- package/dist/contracts/notifications.d.ts +108 -0
- package/dist/contracts/notifications.js +54 -0
- package/dist/contracts/oauth-resources.d.ts +22 -5
- package/dist/contracts/oauth-resources.js +23 -2
- package/dist/contracts/operations.d.ts +0 -48
- package/dist/contracts/operations.js +0 -19
- package/dist/contracts/pages.d.ts +10 -14
- package/dist/contracts/pages.js +17 -21
- 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 +69 -20
- package/dist/contracts/problem-document.d.ts +14 -0
- package/dist/contracts/problem-document.js +35 -0
- package/dist/contracts/push-new.d.ts +41 -2
- package/dist/contracts/push-new.js +4 -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 +16 -71
- package/dist/contracts/resources.js +13 -16
- package/dist/contracts/runtime-api.d.ts +666 -303
- package/dist/contracts/runtime-api.js +272 -113
- package/dist/contracts/runtime-app.d.ts +168 -0
- package/dist/contracts/runtime-app.js +42 -0
- package/dist/contracts/runtime-db.d.ts +424 -0
- package/dist/contracts/runtime-db.js +108 -0
- package/dist/contracts/runtime-storage.d.ts +120 -0
- package/dist/contracts/runtime-storage.js +40 -0
- package/dist/contracts/sf-config-v1.d.ts +54 -1
- package/dist/contracts/sf-config-v1.js +2 -2
- package/dist/contracts/sites.d.ts +6 -0
- package/dist/contracts/sites.js +30 -11
- package/dist/contracts/space-config.d.ts +91 -121
- package/dist/contracts/space-config.js +464 -42
- package/dist/contracts/spaces.d.ts +300 -171
- package/dist/contracts/spaces.js +128 -98
- package/dist/contracts/superadmin-emails.d.ts +2 -2
- package/dist/contracts/superadmin-spaces.d.ts +26 -130
- package/dist/contracts/superadmin-spaces.js +22 -0
- package/dist/contracts/superadmin-tenants.js +4 -4
- package/dist/contracts/superadmin.d.ts +4 -0
- package/dist/contracts/superadmin.js +14 -1
- package/dist/contracts/tags.d.ts +6 -6
- package/dist/contracts/tags.js +1 -1
- package/dist/contracts/teams.d.ts +9 -9
- package/dist/contracts/teams.js +25 -16
- package/dist/contracts/theme-json.js +1 -1
- package/dist/contracts/transfers.d.ts +0 -1
- package/dist/contracts/transfers.js +4 -4
- package/dist/contracts/variables.js +3 -3
- package/dist/contracts/zero.d.ts +384 -596
- package/dist/contracts/zero.js +167 -299
- package/dist/dashboard-paths/index.d.ts +18 -3
- package/dist/dashboard-paths/index.js +19 -0
- package/dist/docs/agent-handoff-document.d.ts +2 -2
- package/dist/docs/agent-handoff-document.js +8 -7
- package/dist/docs/agent-prose.d.ts +22 -3
- package/dist/docs/agent-prose.js +227 -55
- package/dist/docs/agent-setup.d.ts +14 -11
- package/dist/docs/agent-setup.js +289 -61
- package/dist/docs/agent-solutions.d.ts +6 -6
- package/dist/docs/agent-solutions.js +24 -22
- package/dist/docs/catalog.d.ts +53 -98
- package/dist/docs/catalog.js +90 -117
- package/dist/docs/error-docs.js +304 -236
- package/dist/docs/skill-distribution.d.ts +2 -0
- package/dist/docs/skill-distribution.js +7 -4
- package/dist/docs/skills.d.ts +546 -0
- package/dist/docs/skills.js +340 -0
- package/dist/utils/auth-redirect.d.ts +1 -3
- package/dist/utils/auth-redirect.js +1 -21
- package/dist/utils/browser-credential.d.ts +19 -0
- package/dist/utils/browser-credential.js +62 -0
- package/dist/utils/build-settings.d.ts +2 -0
- package/dist/utils/build-settings.js +61 -9
- package/dist/utils/comment-avatars.d.ts +7 -0
- package/dist/utils/comment-avatars.js +42 -0
- package/dist/utils/content-type.d.ts +3 -3
- package/dist/utils/content-type.js +38 -61
- package/dist/utils/credential-policy.d.ts +1 -1
- package/dist/utils/credential-policy.js +4 -0
- package/dist/utils/domain-diagnostics.d.ts +5 -5
- package/dist/utils/domain-diagnostics.js +21 -10
- 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.d.ts +78 -4
- package/dist/utils/local-space-state.js +384 -82
- package/dist/utils/one-shot-replay.d.ts +14 -0
- package/dist/utils/one-shot-replay.js +21 -0
- package/dist/utils/page-preview.d.ts +7 -0
- package/dist/utils/page-preview.js +9 -0
- package/dist/utils/pages.d.ts +2 -5
- package/dist/utils/pages.js +64 -33
- package/dist/utils/publish-form-data.d.ts +22 -0
- package/dist/utils/publish-form-data.js +26 -0
- package/dist/utils/publish-policy.d.ts +34 -4
- package/dist/utils/publish-policy.js +149 -62
- package/dist/utils/query-keys.d.ts +8 -10
- package/dist/utils/query-keys.js +14 -10
- package/dist/utils/runtime-paths.d.ts +3 -0
- package/dist/utils/runtime-paths.js +7 -0
- package/dist/utils/secure-local-file.d.ts +73 -0
- package/dist/utils/secure-local-file.js +1373 -0
- package/dist/utils/space-config.d.ts +17 -4
- package/dist/utils/space-config.js +151 -8
- package/dist/utils/static-runtime-policy.d.ts +5 -0
- package/dist/utils/static-runtime-policy.js +54 -10
- package/dist/utils/storage-policy.d.ts +20 -0
- package/dist/utils/storage-policy.fixtures.json +174 -0
- package/dist/utils/storage-policy.js +193 -0
- package/dist/utils/upload-session.d.ts +15 -1
- package/dist/utils/upload-session.js +68 -2
- package/dist/utils/version-file-browser.d.ts +13 -0
- package/dist/utils/version-file-browser.js +59 -0
- package/package.json +20 -1
- package/dist/contracts/mounts.d.ts +0 -142
- package/dist/contracts/mounts.js +0 -56
- 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/asset-fingerprint.d.ts +0 -53
- package/dist/utils/asset-fingerprint.js +0 -574
- 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
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* Map a file extension to a Content-Type header value. Returns
|
|
3
3
|
* `application/octet-stream` when the extension is not recognized.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* MIME extension coverage comes from the maintained `mime` database. The
|
|
6
|
+
* small overrides above preserve intentional existing policy; this module
|
|
7
|
+
* does not carry a second partial extension registry.
|
|
8
8
|
*/
|
|
9
9
|
export declare function contentTypeForPath(filePath: string): string;
|
|
10
10
|
export declare function textTypeForPath(filePath: string): string | null;
|
|
@@ -1,71 +1,41 @@
|
|
|
1
|
+
import mime from "mime";
|
|
2
|
+
const CONTENT_TYPE_OVERRIDES = {
|
|
3
|
+
// Preserve the public uploader/runtime policy where the database's preferred
|
|
4
|
+
// server-oriented type differs from the established browser-facing type.
|
|
5
|
+
".cjs": "text/javascript",
|
|
6
|
+
".cts": "text/plain",
|
|
7
|
+
".d.ts": "text/plain",
|
|
8
|
+
".ico": "image/x-icon",
|
|
9
|
+
".mts": "text/plain",
|
|
10
|
+
".rs": "text/plain",
|
|
11
|
+
".sh": "text/plain",
|
|
12
|
+
".tgz": "application/gzip",
|
|
13
|
+
".ts": "text/plain",
|
|
14
|
+
};
|
|
15
|
+
const UTF8_APPLICATION_TYPES = new Set([
|
|
16
|
+
"application/json",
|
|
17
|
+
"application/manifest+json",
|
|
18
|
+
"application/xml",
|
|
19
|
+
]);
|
|
1
20
|
/**
|
|
2
21
|
* Map a file extension to a Content-Type header value. Returns
|
|
3
22
|
* `application/octet-stream` when the extension is not recognized.
|
|
4
23
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
24
|
+
* MIME extension coverage comes from the maintained `mime` database. The
|
|
25
|
+
* small overrides above preserve intentional existing policy; this module
|
|
26
|
+
* does not carry a second partial extension registry.
|
|
8
27
|
*/
|
|
9
28
|
export function contentTypeForPath(filePath) {
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
return "text/html; charset=utf-8";
|
|
16
|
-
case ".css":
|
|
17
|
-
return "text/css; charset=utf-8";
|
|
18
|
-
case ".js":
|
|
19
|
-
case ".mjs":
|
|
20
|
-
case ".cjs":
|
|
21
|
-
return "text/javascript; charset=utf-8";
|
|
22
|
-
case ".json":
|
|
23
|
-
case ".map":
|
|
24
|
-
return "application/json; charset=utf-8";
|
|
25
|
-
case ".webmanifest":
|
|
26
|
-
return "application/manifest+json; charset=utf-8";
|
|
27
|
-
case ".gz":
|
|
28
|
-
case ".tgz":
|
|
29
|
-
return "application/gzip";
|
|
30
|
-
case ".txt":
|
|
31
|
-
case ".sh":
|
|
32
|
-
return "text/plain; charset=utf-8";
|
|
33
|
-
case ".md":
|
|
34
|
-
return "text/markdown; charset=utf-8";
|
|
35
|
-
case ".csv":
|
|
36
|
-
return "text/csv; charset=utf-8";
|
|
37
|
-
case ".xml":
|
|
38
|
-
return "application/xml; charset=utf-8";
|
|
39
|
-
case ".svg":
|
|
40
|
-
return "image/svg+xml";
|
|
41
|
-
case ".png":
|
|
42
|
-
return "image/png";
|
|
43
|
-
case ".jpg":
|
|
44
|
-
case ".jpeg":
|
|
45
|
-
return "image/jpeg";
|
|
46
|
-
case ".gif":
|
|
47
|
-
return "image/gif";
|
|
48
|
-
case ".webp":
|
|
49
|
-
return "image/webp";
|
|
50
|
-
case ".avif":
|
|
51
|
-
return "image/avif";
|
|
52
|
-
case ".ico":
|
|
53
|
-
return "image/x-icon";
|
|
54
|
-
case ".pdf":
|
|
55
|
-
return "application/pdf";
|
|
56
|
-
case ".woff":
|
|
57
|
-
return "font/woff";
|
|
58
|
-
case ".woff2":
|
|
59
|
-
return "font/woff2";
|
|
60
|
-
case ".ttf":
|
|
61
|
-
return "font/ttf";
|
|
62
|
-
case ".otf":
|
|
63
|
-
return "font/otf";
|
|
64
|
-
case ".wasm":
|
|
65
|
-
return "application/wasm";
|
|
66
|
-
default:
|
|
67
|
-
return "application/octet-stream";
|
|
29
|
+
const normalizedPath = filePath.toLowerCase();
|
|
30
|
+
const override = Object.entries(CONTENT_TYPE_OVERRIDES).find(([suffix]) => normalizedPath.endsWith(suffix))?.[1];
|
|
31
|
+
const type = override ?? mime.getType(filePath);
|
|
32
|
+
if (!type) {
|
|
33
|
+
return "application/octet-stream";
|
|
68
34
|
}
|
|
35
|
+
if (type.startsWith("text/") || UTF8_APPLICATION_TYPES.has(type)) {
|
|
36
|
+
return `${type}; charset=utf-8`;
|
|
37
|
+
}
|
|
38
|
+
return type;
|
|
69
39
|
}
|
|
70
40
|
function hasPrefix(bytes, prefix) {
|
|
71
41
|
return prefix.every((value, index) => bytes[index] === value);
|
|
@@ -141,5 +111,12 @@ export function contentTypeForBytes(filePath, bytes) {
|
|
|
141
111
|
if (textType && bytesLookTextual(bytes)) {
|
|
142
112
|
return textType;
|
|
143
113
|
}
|
|
114
|
+
// Container signatures such as ISO-BMFF, WebM, and Ogg are shared by audio
|
|
115
|
+
// and video variants. The extension is the reliable discriminator for the
|
|
116
|
+
// manifest declaration, while unknown binary extensions remain generic.
|
|
117
|
+
const pathType = contentTypeForPath(filePath);
|
|
118
|
+
if (pathType.startsWith("audio/") || pathType.startsWith("video/")) {
|
|
119
|
+
return pathType;
|
|
120
|
+
}
|
|
144
121
|
return "application/octet-stream";
|
|
145
122
|
}
|
|
@@ -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,15 +1,15 @@
|
|
|
1
1
|
export type HostnameDiagnosticsForActions = {
|
|
2
2
|
hostname: string;
|
|
3
|
-
providerIps
|
|
4
|
-
dnsInstructions
|
|
3
|
+
providerIps?: string[] | undefined;
|
|
4
|
+
dnsInstructions?: unknown[] | undefined;
|
|
5
5
|
dns: {
|
|
6
6
|
pointsToProvider: boolean;
|
|
7
7
|
txtVerified: boolean | null;
|
|
8
8
|
expectedTxtValue: string | null;
|
|
9
9
|
records?: {
|
|
10
|
-
a
|
|
11
|
-
aaaa
|
|
12
|
-
};
|
|
10
|
+
a?: string[] | undefined;
|
|
11
|
+
aaaa?: string[] | undefined;
|
|
12
|
+
} | undefined;
|
|
13
13
|
};
|
|
14
14
|
ssl: {
|
|
15
15
|
status: string;
|
|
@@ -3,8 +3,20 @@ import { addressDnsInstructionIsObserved, addressDnsInstructionsForHostname, uns
|
|
|
3
3
|
export function domainRequiredActions(hostnameDiagnostics) {
|
|
4
4
|
const actions = [];
|
|
5
5
|
for (const entry of hostnameDiagnostics) {
|
|
6
|
-
const
|
|
7
|
-
?
|
|
6
|
+
const records = entry.dns.records
|
|
7
|
+
? {
|
|
8
|
+
a: entry.dns.records.a ?? [],
|
|
9
|
+
aaaa: entry.dns.records.aaaa ?? [],
|
|
10
|
+
}
|
|
11
|
+
: undefined;
|
|
12
|
+
const dnsInstructions = entry.dnsInstructions ?? [];
|
|
13
|
+
const providerIps = entry.providerIps ?? [];
|
|
14
|
+
const unsupportedRecords = records
|
|
15
|
+
? unsupportedAddressDnsRecordsForHostname({
|
|
16
|
+
hostname: entry.hostname,
|
|
17
|
+
dnsInstructions,
|
|
18
|
+
records,
|
|
19
|
+
})
|
|
8
20
|
: [];
|
|
9
21
|
actions.push(...unsupportedRecords.map((record) => ({
|
|
10
22
|
code: "dns_unaccepted_address",
|
|
@@ -12,13 +24,12 @@ export function domainRequiredActions(hostnameDiagnostics) {
|
|
|
12
24
|
message: `Remove the ${record.type} record on ${record.name} with value ${record.value}; it does not match a Spacefast target for this hostname.`,
|
|
13
25
|
})));
|
|
14
26
|
if (!entry.dns.pointsToProvider) {
|
|
15
|
-
const routingInstructions = addressDnsInstructionsForHostname(
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}))
|
|
27
|
+
const routingInstructions = addressDnsInstructionsForHostname({
|
|
28
|
+
hostname: entry.hostname,
|
|
29
|
+
dnsInstructions,
|
|
30
|
+
});
|
|
31
|
+
const missingRoutingInstructions = records
|
|
32
|
+
? routingInstructions.filter((instruction) => !addressDnsInstructionIsObserved({ instruction, records }))
|
|
22
33
|
: routingInstructions;
|
|
23
34
|
if (missingRoutingInstructions.length > 0) {
|
|
24
35
|
actions.push(...missingRoutingInstructions.map((instruction) => ({
|
|
@@ -28,7 +39,7 @@ export function domainRequiredActions(hostnameDiagnostics) {
|
|
|
28
39
|
})));
|
|
29
40
|
}
|
|
30
41
|
else if (routingInstructions.length === 0) {
|
|
31
|
-
const targets =
|
|
42
|
+
const targets = providerIps.length > 0 ? providerIps.join(", ") : "Spacefast";
|
|
32
43
|
actions.push({
|
|
33
44
|
code: "dns_not_pointing",
|
|
34
45
|
hostname: entry.hostname,
|
|
@@ -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
|
+
}
|
|
@@ -13,15 +13,18 @@ export declare const LOCAL_SPACE_STATE_FILE_NAME = "state.json";
|
|
|
13
13
|
* `.spacefast/state.json` is the local credential/cache axis: it records the
|
|
14
14
|
* resolved `spaceId`, provider `apiUrl`, anonymous `claimToken` before claim,
|
|
15
15
|
* durable `accessToken` after continuation, and small publish caches such as
|
|
16
|
-
* `lastVersionId`. It is secret, machine-written, gitignored, and merge-written
|
|
17
|
-
*
|
|
16
|
+
* `lastVersionId`. It is secret, machine-written, gitignored, and merge-written.
|
|
17
|
+
* The strict versioned schema fails closed instead of letting an older tool
|
|
18
|
+
* reinterpret fields written under a contract it does not understand.
|
|
18
19
|
*/
|
|
19
20
|
export declare const localSpaceLinkSchema: z.ZodObject<{
|
|
20
21
|
team: z.ZodOptional<z.ZodString>;
|
|
21
22
|
space: z.ZodString;
|
|
22
23
|
apiBaseUrl: z.ZodOptional<z.ZodString>;
|
|
23
24
|
}, z.core.$loose>;
|
|
25
|
+
export declare const LOCAL_SPACE_STATE_SCHEMA_VERSION = 1;
|
|
24
26
|
export declare const localSpaceStateSchema: z.ZodObject<{
|
|
27
|
+
schemaVersion: z.ZodLiteral<1>;
|
|
25
28
|
spaceId: z.ZodOptional<z.ZodString>;
|
|
26
29
|
apiUrl: z.ZodOptional<z.ZodString>;
|
|
27
30
|
teamSlug: z.ZodOptional<z.ZodString>;
|
|
@@ -30,11 +33,52 @@ export declare const localSpaceStateSchema: z.ZodObject<{
|
|
|
30
33
|
expiresAt: z.ZodOptional<z.ZodString>;
|
|
31
34
|
accessToken: z.ZodOptional<z.ZodString>;
|
|
32
35
|
lastVersionId: z.ZodOptional<z.ZodString>;
|
|
36
|
+
pendingOperationId: z.ZodOptional<z.ZodString>;
|
|
33
37
|
lastVersionUrl: z.ZodOptional<z.ZodString>;
|
|
34
38
|
lastSpaceUrl: z.ZodOptional<z.ZodString>;
|
|
35
|
-
|
|
39
|
+
/** Secret per-checkout entropy used only to namespace replay-safe first publishes. */
|
|
40
|
+
anonymousPublishNonce: z.ZodOptional<z.ZodString>;
|
|
41
|
+
/** Unsettled first-publish mutation. Retries must reuse this exact key. */
|
|
42
|
+
pendingPublish: z.ZodOptional<z.ZodObject<{
|
|
43
|
+
key: z.ZodString;
|
|
44
|
+
principal: z.ZodString;
|
|
45
|
+
kind: z.ZodEnum<{
|
|
46
|
+
anonymous: "anonymous";
|
|
47
|
+
authenticated: "authenticated";
|
|
48
|
+
}>;
|
|
49
|
+
intent: z.ZodOptional<z.ZodString>;
|
|
50
|
+
createdAt: z.ZodString;
|
|
51
|
+
}, z.core.$strict>>;
|
|
52
|
+
/** Settled creation receipt identity retained for invocations that began before its commit. */
|
|
53
|
+
creationReplay: z.ZodOptional<z.ZodObject<{
|
|
54
|
+
key: z.ZodString;
|
|
55
|
+
principal: z.ZodString;
|
|
56
|
+
kind: z.ZodEnum<{
|
|
57
|
+
anonymous: "anonymous";
|
|
58
|
+
authenticated: "authenticated";
|
|
59
|
+
}>;
|
|
60
|
+
intent: z.ZodOptional<z.ZodString>;
|
|
61
|
+
createdAt: z.ZodString;
|
|
62
|
+
}, z.core.$strict>>;
|
|
63
|
+
/** Random replay capability reserved before consuming a claim credential. */
|
|
64
|
+
pendingCredentialReplay: z.ZodOptional<z.ZodObject<{
|
|
65
|
+
operation: z.ZodEnum<{
|
|
66
|
+
"claim-rotate": "claim-rotate";
|
|
67
|
+
"claim-exchange": "claim-exchange";
|
|
68
|
+
}>;
|
|
69
|
+
credentialFingerprint: z.ZodString;
|
|
70
|
+
key: z.ZodString;
|
|
71
|
+
principal: z.ZodString;
|
|
72
|
+
createdAt: z.ZodString;
|
|
73
|
+
}, z.core.$strict>>;
|
|
74
|
+
}, z.core.$strict>;
|
|
36
75
|
export type LocalSpaceLink = z.infer<typeof localSpaceLinkSchema>;
|
|
37
76
|
export type LocalSpaceStateFile = z.infer<typeof localSpaceStateSchema>;
|
|
77
|
+
export type LocalSpaceStateWrite = Omit<LocalSpaceStateFile, "schemaVersion"> & {
|
|
78
|
+
schemaVersion?: typeof LOCAL_SPACE_STATE_SCHEMA_VERSION;
|
|
79
|
+
};
|
|
80
|
+
export type LocalCredentialReplay = NonNullable<LocalSpaceStateFile["pendingCredentialReplay"]>;
|
|
81
|
+
export type LocalCredentialReplayOperation = LocalCredentialReplay["operation"];
|
|
38
82
|
export type DiscoveredLocalSpaceState = {
|
|
39
83
|
root: string;
|
|
40
84
|
statePath: string | null;
|
|
@@ -48,6 +92,7 @@ export type DiscoveredLocalSpaceState = {
|
|
|
48
92
|
claimUrl: string | undefined;
|
|
49
93
|
expiresAt: string | undefined;
|
|
50
94
|
lastVersionId: string | undefined;
|
|
95
|
+
pendingOperationId: string | undefined;
|
|
51
96
|
};
|
|
52
97
|
export type SaveLocalSpaceStateInput = {
|
|
53
98
|
spaceId: string;
|
|
@@ -58,9 +103,26 @@ export type SaveLocalSpaceStateInput = {
|
|
|
58
103
|
expiresAt?: string | undefined;
|
|
59
104
|
accessToken?: string | undefined;
|
|
60
105
|
lastVersionId?: string | undefined;
|
|
106
|
+
pendingOperationId?: string | undefined;
|
|
61
107
|
lastVersionUrl?: string | undefined;
|
|
62
108
|
lastSpaceUrl?: string | undefined;
|
|
109
|
+
/** Required when replacing one shown-once credential with another. */
|
|
110
|
+
expectedCredential?: {
|
|
111
|
+
claimToken?: string | undefined;
|
|
112
|
+
accessToken?: string | undefined;
|
|
113
|
+
};
|
|
114
|
+
/** Replay reservation completed by this shown-once credential replacement. */
|
|
115
|
+
credentialReplay?: LocalCredentialReplay | undefined;
|
|
116
|
+
/** Explicit relinks may replace another Space; ordinary cache writes may not. */
|
|
117
|
+
replaceSpace?: boolean | undefined;
|
|
63
118
|
};
|
|
119
|
+
export declare class LocalSpaceStateConflictError extends Error {
|
|
120
|
+
name: string;
|
|
121
|
+
}
|
|
122
|
+
export declare class LocalSpaceStateError extends Error {
|
|
123
|
+
readonly path: string;
|
|
124
|
+
constructor(filePath: string, cause?: unknown);
|
|
125
|
+
}
|
|
64
126
|
export declare function localSpaceStateDir(root: string): string;
|
|
65
127
|
export declare function localSpaceStatePath(root: string): string;
|
|
66
128
|
export declare function localSpaceLinkPath(root: string): string;
|
|
@@ -83,7 +145,19 @@ export declare function readLocalSpaceStateAt(root: string): Promise<DiscoveredL
|
|
|
83
145
|
export declare function findUpward<T>(probe: (dir: string) => Promise<T | null>, startDir?: string, boundary?: string): Promise<T | null>;
|
|
84
146
|
export declare function discoverLocalSpaceState(startDir?: string, boundary?: string): Promise<DiscoveredLocalSpaceState | null>;
|
|
85
147
|
export declare function writeLocalSpaceLink(root: string, link: LocalSpaceLink): Promise<string>;
|
|
86
|
-
export declare function
|
|
148
|
+
export declare function mutateLocalSpaceState(root: string, mutate: (current: LocalSpaceStateFile) => LocalSpaceStateWrite, options?: {
|
|
149
|
+
beforeCommit?: (() => void | Promise<void>) | undefined;
|
|
150
|
+
afterBackup?: (() => void | Promise<void>) | undefined;
|
|
151
|
+
/** Fault-injection observer used to synchronize concurrency tests. */
|
|
152
|
+
onConflict?: (() => void | Promise<void>) | undefined;
|
|
153
|
+
}): Promise<LocalSpaceStateFile>;
|
|
154
|
+
export declare function writeLocalSpaceState(root: string, state: LocalSpaceStateWrite): Promise<string>;
|
|
155
|
+
/** Reserve random replay custody before a claim token can be consumed. */
|
|
156
|
+
export declare function reserveLocalCredentialReplay(root: string, input: {
|
|
157
|
+
operation: LocalCredentialReplayOperation;
|
|
158
|
+
claimToken: string;
|
|
159
|
+
allowUnstoredClaimToken?: boolean | undefined;
|
|
160
|
+
}): Promise<LocalCredentialReplay>;
|
|
87
161
|
export declare function saveLocalSpaceState(root: string, input: SaveLocalSpaceStateInput): Promise<{
|
|
88
162
|
statePath: string;
|
|
89
163
|
linkPath: string;
|