@spacefast/common 0.0.11 → 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/agents/connect-targets.d.ts +4 -4
- package/dist/agents/connect-targets.js +3 -3
- package/dist/agents/private-key-oauth.d.ts +37 -0
- package/dist/agents/private-key-oauth.js +158 -0
- package/dist/config/domains.js +1 -1
- package/dist/config/index.d.ts +1 -0
- package/dist/config/index.js +1 -0
- package/dist/config/postgres-budget.d.ts +126 -0
- package/dist/config/postgres-budget.js +135 -0
- package/dist/config/postgres-budget.json +50 -0
- package/dist/contracts/access.d.ts +386 -1183
- package/dist/contracts/access.js +240 -1018
- package/dist/contracts/activity.d.ts +1 -1
- package/dist/contracts/activity.js +11 -19
- package/dist/contracts/api-keys.d.ts +69 -45
- package/dist/contracts/api-keys.js +3 -0
- package/dist/contracts/archives.d.ts +9 -10
- package/dist/contracts/archives.js +4 -6
- package/dist/contracts/builds.d.ts +8 -8
- package/dist/contracts/builds.js +28 -48
- package/dist/contracts/channels.js +1 -2
- package/dist/contracts/comments.d.ts +102 -93
- package/dist/contracts/comments.js +121 -63
- package/dist/contracts/common.d.ts +4 -3
- package/dist/contracts/common.js +2 -0
- 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/deployments.d.ts +3 -3
- package/dist/contracts/device-auth.d.ts +21 -15
- package/dist/contracts/docs.d.ts +5 -5
- package/dist/contracts/domains.d.ts +261 -1330
- package/dist/contracts/domains.js +39 -301
- package/dist/contracts/email-preferences.d.ts +50 -0
- package/dist/contracts/email-preferences.js +44 -0
- package/dist/contracts/enums.d.ts +3 -5
- package/dist/contracts/enums.js +3 -8
- package/dist/contracts/error-code-meta.d.ts +99 -33
- package/dist/contracts/error-code-meta.js +36 -9
- package/dist/contracts/error-codes.d.ts +1 -1
- package/dist/contracts/error-codes.js +28 -11
- package/dist/contracts/execution.d.ts +212 -0
- package/dist/contracts/execution.js +288 -0
- package/dist/contracts/feature-lifecycle.d.ts +498 -2
- package/dist/contracts/feature-lifecycle.js +539 -2
- package/dist/contracts/features.d.ts +184 -74
- package/dist/contracts/features.js +4 -303
- 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 +6 -5
- package/dist/contracts/ids.js +6 -5
- package/dist/contracts/internal.d.ts +3 -3
- package/dist/contracts/mcp.d.ts +12 -12
- package/dist/contracts/me.js +1 -1
- package/dist/contracts/mounts.d.ts +142 -0
- package/dist/contracts/mounts.js +56 -0
- package/dist/contracts/oauth-resources.d.ts +5 -5
- package/dist/contracts/oauth-resources.js +2 -0
- package/dist/contracts/operations.d.ts +4 -4
- package/dist/contracts/pages.d.ts +6 -14
- package/dist/contracts/pages.js +11 -23
- package/dist/contracts/plan-policy.d.ts +2 -9
- package/dist/contracts/plan-policy.js +1 -4
- package/dist/contracts/platform.d.ts +80 -32
- 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 +21 -85
- package/dist/contracts/resources.js +17 -47
- package/dist/contracts/runtime-api.d.ts +799 -192
- package/dist/contracts/runtime-api.js +231 -100
- 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/sf-config-v1.js +1 -1
- package/dist/contracts/sites.d.ts +17 -12
- package/dist/contracts/sites.js +30 -12
- package/dist/contracts/space-config.d.ts +46 -117
- package/dist/contracts/space-config.js +323 -25
- package/dist/contracts/spaces.d.ts +392 -256
- package/dist/contracts/spaces.js +163 -83
- package/dist/contracts/superadmin-emails.d.ts +27 -3
- package/dist/contracts/superadmin-emails.js +10 -15
- package/dist/contracts/superadmin-runtime.d.ts +2 -2
- package/dist/contracts/superadmin-spaces.d.ts +19 -142
- package/dist/contracts/superadmin-spaces.js +7 -0
- package/dist/contracts/tags.d.ts +66 -36
- package/dist/contracts/tags.js +195 -11
- package/dist/contracts/teams.d.ts +9 -9
- package/dist/contracts/transfers.d.ts +1 -2
- package/dist/contracts/variables.d.ts +4 -4
- package/dist/contracts/variables.js +4 -3
- package/dist/contracts/webhooks.d.ts +2 -2
- package/dist/contracts/zero.d.ts +437 -193
- package/dist/contracts/zero.js +149 -158
- package/dist/dashboard-paths/index.d.ts +3 -3
- package/dist/dashboard-paths/index.js +1 -0
- package/dist/docs/agent-prose.js +10 -8
- package/dist/docs/agent-setup.d.ts +6 -4
- package/dist/docs/agent-setup.js +26 -16
- package/dist/docs/agent-solutions.js +1 -1
- package/dist/docs/catalog.d.ts +35 -37
- package/dist/docs/catalog.js +36 -38
- package/dist/docs/error-docs.js +108 -44
- 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/dns-instructions.d.ts +29 -0
- package/dist/utils/dns-instructions.js +78 -0
- package/dist/utils/domain-diagnostics.d.ts +29 -0
- package/dist/utils/domain-diagnostics.js +63 -0
- package/dist/utils/egress-policy.fixtures.json +51 -0
- package/dist/utils/grants.d.ts +53 -0
- package/dist/utils/grants.js +160 -0
- package/dist/utils/id-hints.d.ts +1 -0
- package/dist/utils/id-hints.js +55 -13
- package/dist/utils/local-space-state.js +1 -0
- package/dist/utils/page-colors.d.ts +50 -0
- package/dist/utils/page-colors.js +167 -0
- package/dist/utils/page-fonts.d.ts +2 -0
- package/dist/utils/page-fonts.js +4 -0
- package/dist/utils/pages.d.ts +19 -6
- package/dist/utils/pages.js +122 -49
- package/dist/utils/production-runtime.d.ts +18 -0
- package/dist/utils/production-runtime.js +28 -0
- package/dist/utils/publish-policy.d.ts +34 -4
- package/dist/utils/publish-policy.js +132 -61
- package/dist/utils/query-keys.d.ts +7 -13
- package/dist/utils/query-keys.js +12 -18
- package/dist/utils/source-archive-policy.d.ts +2 -0
- package/dist/utils/source-archive-policy.js +2 -0
- 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.fixtures.json +57 -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 +6 -4
- package/dist/contracts/annotations.d.ts +0 -327
- package/dist/contracts/annotations.js +0 -235
- package/dist/contracts/generated-feature-launch-entries.d.ts +0 -2
- package/dist/contracts/generated-feature-launch-entries.js +0 -518
- package/dist/contracts/publishes.d.ts +0 -135
- package/dist/contracts/publishes.js +0 -162
- package/dist/utils/access-match.d.ts +0 -39
- package/dist/utils/access-match.js +0 -296
- package/dist/utils/sf-config-v1.d.ts +0 -2
- package/dist/utils/sf-config-v1.js +0 -542
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { DnsInstructionSchema } from "../contracts/common.js";
|
|
2
|
+
function normalizeDnsName(value) {
|
|
3
|
+
return value.trim().toLowerCase().replace(/\.$/, "");
|
|
4
|
+
}
|
|
5
|
+
export function addressDnsInstructionsForHostname(input) {
|
|
6
|
+
const hostname = normalizeDnsName(input.hostname);
|
|
7
|
+
return input.dnsInstructions.flatMap((candidate) => {
|
|
8
|
+
const parsed = DnsInstructionSchema.safeParse(candidate);
|
|
9
|
+
if (!parsed.success) {
|
|
10
|
+
return [];
|
|
11
|
+
}
|
|
12
|
+
const instruction = parsed.data;
|
|
13
|
+
if ((instruction.type !== "A" && instruction.type !== "AAAA") ||
|
|
14
|
+
instruction.purpose !== "routing" ||
|
|
15
|
+
normalizeDnsName(instruction.name) !== hostname) {
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
return [
|
|
19
|
+
{
|
|
20
|
+
type: instruction.type,
|
|
21
|
+
name: instruction.name,
|
|
22
|
+
value: instruction.value,
|
|
23
|
+
purpose: instruction.purpose,
|
|
24
|
+
...(instruction.ttl === undefined ? {} : { ttl: instruction.ttl }),
|
|
25
|
+
},
|
|
26
|
+
];
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
function normalizeAddress(value) {
|
|
30
|
+
const address = value.trim().toLowerCase();
|
|
31
|
+
if (!address.includes(":")) {
|
|
32
|
+
return address;
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
return new URL(`http://[${address}]`).hostname.slice(1, -1);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return address;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export function isWpCloudInboundIpv4(value) {
|
|
42
|
+
const match = /^192\.0\.79\.(\d{1,3})$/.exec(value);
|
|
43
|
+
if (!match) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
const octet = Number(match[1]);
|
|
47
|
+
return octet >= 128 && octet <= 191;
|
|
48
|
+
}
|
|
49
|
+
function instructionAcceptsObservedAddress(instruction, observedValue) {
|
|
50
|
+
const expected = normalizeAddress(instruction.value);
|
|
51
|
+
const observed = normalizeAddress(observedValue);
|
|
52
|
+
return observed === expected || (instruction.type === "A" && isWpCloudInboundIpv4(observed));
|
|
53
|
+
}
|
|
54
|
+
export function addressDnsInstructionIsObserved(input) {
|
|
55
|
+
const observed = input.instruction.type === "A" ? input.records.a : input.records.aaaa;
|
|
56
|
+
return observed.some((value) => instructionAcceptsObservedAddress(input.instruction, value));
|
|
57
|
+
}
|
|
58
|
+
export function unsupportedAddressDnsRecordsForHostname(input) {
|
|
59
|
+
const instructions = addressDnsInstructionsForHostname(input);
|
|
60
|
+
if (instructions.length === 0) {
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
const records = [
|
|
64
|
+
...input.records.a.map((value) => ({ type: "A", value })),
|
|
65
|
+
...input.records.aaaa.map((value) => ({ type: "AAAA", value })),
|
|
66
|
+
].filter((record) => !instructions.some((instruction) => instruction.type === record.type &&
|
|
67
|
+
instructionAcceptsObservedAddress(instruction, record.value)));
|
|
68
|
+
const unique = new Set();
|
|
69
|
+
return records.flatMap((record) => {
|
|
70
|
+
const value = record.value.trim();
|
|
71
|
+
const key = `${record.type}:${value}`;
|
|
72
|
+
if (!value || unique.has(key)) {
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
unique.add(key);
|
|
76
|
+
return [{ ...record, name: input.hostname }];
|
|
77
|
+
});
|
|
78
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export type HostnameDiagnosticsForActions = {
|
|
2
|
+
hostname: string;
|
|
3
|
+
providerIps: string[];
|
|
4
|
+
dnsInstructions: unknown[];
|
|
5
|
+
dns: {
|
|
6
|
+
pointsToProvider: boolean;
|
|
7
|
+
txtVerified: boolean | null;
|
|
8
|
+
expectedTxtValue: string | null;
|
|
9
|
+
records?: {
|
|
10
|
+
a: string[];
|
|
11
|
+
aaaa: string[];
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
ssl: {
|
|
15
|
+
status: string;
|
|
16
|
+
canRetry: boolean;
|
|
17
|
+
brokenRecord: {
|
|
18
|
+
reason: string | null;
|
|
19
|
+
lastError: string | null;
|
|
20
|
+
} | null;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
export type DomainRequiredAction = {
|
|
24
|
+
code: string;
|
|
25
|
+
hostname: string;
|
|
26
|
+
message: string;
|
|
27
|
+
};
|
|
28
|
+
/** Turn hostname diagnostics into the actions a user must take to finish binding. */
|
|
29
|
+
export declare function domainRequiredActions(hostnameDiagnostics: HostnameDiagnosticsForActions[]): DomainRequiredAction[];
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { addressDnsInstructionIsObserved, addressDnsInstructionsForHostname, unsupportedAddressDnsRecordsForHostname, } from "./dns-instructions.js";
|
|
2
|
+
/** Turn hostname diagnostics into the actions a user must take to finish binding. */
|
|
3
|
+
export function domainRequiredActions(hostnameDiagnostics) {
|
|
4
|
+
const actions = [];
|
|
5
|
+
for (const entry of hostnameDiagnostics) {
|
|
6
|
+
const unsupportedRecords = entry.dns.records
|
|
7
|
+
? unsupportedAddressDnsRecordsForHostname({ ...entry, records: entry.dns.records })
|
|
8
|
+
: [];
|
|
9
|
+
actions.push(...unsupportedRecords.map((record) => ({
|
|
10
|
+
code: "dns_unaccepted_address",
|
|
11
|
+
hostname: entry.hostname,
|
|
12
|
+
message: `Remove the ${record.type} record on ${record.name} with value ${record.value}; it does not match a Spacefast target for this hostname.`,
|
|
13
|
+
})));
|
|
14
|
+
if (!entry.dns.pointsToProvider) {
|
|
15
|
+
const routingInstructions = addressDnsInstructionsForHostname(entry);
|
|
16
|
+
const observedAddressRecords = entry.dns.records;
|
|
17
|
+
const missingRoutingInstructions = observedAddressRecords
|
|
18
|
+
? routingInstructions.filter((instruction) => !addressDnsInstructionIsObserved({
|
|
19
|
+
instruction,
|
|
20
|
+
records: observedAddressRecords,
|
|
21
|
+
}))
|
|
22
|
+
: routingInstructions;
|
|
23
|
+
if (missingRoutingInstructions.length > 0) {
|
|
24
|
+
actions.push(...missingRoutingInstructions.map((instruction) => ({
|
|
25
|
+
code: "dns_not_pointing",
|
|
26
|
+
hostname: entry.hostname,
|
|
27
|
+
message: `Add an ${instruction.type} record on ${instruction.name} with value ${instruction.value}.`,
|
|
28
|
+
})));
|
|
29
|
+
}
|
|
30
|
+
else if (routingInstructions.length === 0) {
|
|
31
|
+
const targets = entry.providerIps.length > 0 ? entry.providerIps.join(", ") : "Spacefast";
|
|
32
|
+
actions.push({
|
|
33
|
+
code: "dns_not_pointing",
|
|
34
|
+
hostname: entry.hostname,
|
|
35
|
+
message: `Point ${entry.hostname} at ${targets} using the DNS record type shown by Spacefast.`,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (entry.dns.txtVerified === false && entry.dns.expectedTxtValue) {
|
|
40
|
+
actions.push({
|
|
41
|
+
code: "txt_verification_missing",
|
|
42
|
+
hostname: entry.hostname,
|
|
43
|
+
message: `Add a TXT record on ${entry.hostname} with value ${entry.dns.expectedTxtValue}.`,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
if (entry.ssl.brokenRecord) {
|
|
47
|
+
const reason = entry.ssl.brokenRecord.reason ?? entry.ssl.brokenRecord.lastError;
|
|
48
|
+
actions.push({
|
|
49
|
+
code: "ssl_blocking_record",
|
|
50
|
+
hostname: entry.hostname,
|
|
51
|
+
message: `Fix the DNS record blocking SSL on ${entry.hostname}${reason ? ` (${reason})` : ""}.`,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
else if (entry.ssl.status !== "active" && entry.ssl.canRetry) {
|
|
55
|
+
actions.push({
|
|
56
|
+
code: "ssl_retry_available",
|
|
57
|
+
hostname: entry.hostname,
|
|
58
|
+
message: `SSL is ${entry.ssl.status} on ${entry.hostname}; retry with \`sf domains check ${entry.hostname}\`.`,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return actions;
|
|
63
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
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
|
+
"ips": [
|
|
4
|
+
{ "ip": "127.0.0.1", "public": false, "reason": "loopback" },
|
|
5
|
+
{ "ip": "127.8.9.10", "public": false, "reason": "loopback /8" },
|
|
6
|
+
{ "ip": "0.0.0.0", "public": false, "reason": "this-network" },
|
|
7
|
+
{ "ip": "10.0.0.1", "public": false, "reason": "RFC1918" },
|
|
8
|
+
{ "ip": "172.16.0.1", "public": false, "reason": "RFC1918" },
|
|
9
|
+
{ "ip": "172.31.255.255", "public": false, "reason": "RFC1918 upper bound" },
|
|
10
|
+
{ "ip": "172.32.0.1", "public": true, "reason": "outside 172.16/12" },
|
|
11
|
+
{ "ip": "192.168.1.1", "public": false, "reason": "RFC1918" },
|
|
12
|
+
{ "ip": "100.64.0.1", "public": false, "reason": "carrier-grade NAT" },
|
|
13
|
+
{ "ip": "169.254.169.254", "public": false, "reason": "cloud metadata" },
|
|
14
|
+
{ "ip": "169.254.0.1", "public": false, "reason": "link-local" },
|
|
15
|
+
{ "ip": "192.0.0.1", "public": false, "reason": "special-purpose" },
|
|
16
|
+
{ "ip": "192.0.2.1", "public": false, "reason": "TEST-NET-1" },
|
|
17
|
+
{ "ip": "198.18.0.1", "public": false, "reason": "benchmarking" },
|
|
18
|
+
{ "ip": "198.51.100.7", "public": false, "reason": "TEST-NET-2" },
|
|
19
|
+
{ "ip": "203.0.113.9", "public": false, "reason": "TEST-NET-3" },
|
|
20
|
+
{ "ip": "224.0.0.1", "public": false, "reason": "multicast" },
|
|
21
|
+
{ "ip": "255.255.255.255", "public": false, "reason": "broadcast" },
|
|
22
|
+
{ "ip": "8.8.8.8", "public": true, "reason": "public" },
|
|
23
|
+
{ "ip": "1.1.1.1", "public": true, "reason": "public" },
|
|
24
|
+
{ "ip": "93.184.216.34", "public": true, "reason": "public" },
|
|
25
|
+
{ "ip": "::1", "public": false, "reason": "loopback" },
|
|
26
|
+
{ "ip": "::", "public": false, "reason": "unspecified" },
|
|
27
|
+
{ "ip": "fe80::1", "public": false, "reason": "link-local" },
|
|
28
|
+
{ "ip": "fc00::1", "public": false, "reason": "unique-local" },
|
|
29
|
+
{ "ip": "fd00:ec2::254", "public": false, "reason": "ULA cloud metadata" },
|
|
30
|
+
{ "ip": "ff02::1", "public": false, "reason": "multicast" },
|
|
31
|
+
{ "ip": "2001:db8::1", "public": false, "reason": "documentation" },
|
|
32
|
+
{ "ip": "100::1", "public": false, "reason": "discard-only" },
|
|
33
|
+
{ "ip": "64:ff9b::808:808", "public": false, "reason": "NAT64 denied outright" },
|
|
34
|
+
{ "ip": "::ffff:10.0.0.1", "public": false, "reason": "IPv4-mapped private" },
|
|
35
|
+
{ "ip": "::ffff:a00:1", "public": false, "reason": "IPv4-mapped private (hex form)" },
|
|
36
|
+
{
|
|
37
|
+
"ip": "::ffff:8.8.8.8",
|
|
38
|
+
"public": true,
|
|
39
|
+
"reason": "IPv4-mapped public defers to inner policy"
|
|
40
|
+
},
|
|
41
|
+
{ "ip": "2606:4700:4700::1111", "public": true, "reason": "public" }
|
|
42
|
+
],
|
|
43
|
+
"hosts": [
|
|
44
|
+
{ "host": "localhost", "allowed": false },
|
|
45
|
+
{ "host": "sub.localhost", "allowed": false },
|
|
46
|
+
{ "host": "169.254.169.254", "allowed": false },
|
|
47
|
+
{ "host": "view.fast", "allowed": false },
|
|
48
|
+
{ "host": "wpc-manage-site-123.view.fast", "allowed": false },
|
|
49
|
+
{ "host": "example.com", "allowed": true }
|
|
50
|
+
]
|
|
51
|
+
}
|
|
@@ -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
|
+
}
|
package/dist/utils/id-hints.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export declare const HINT_SCAN_CAP = 200;
|
|
2
2
|
export declare const HINT_MAX_CANDIDATES = 5;
|
|
3
|
+
export declare const HINT_MAX_KEY_LENGTH = 256;
|
|
3
4
|
export declare function levenshtein(a: string, b: string): number;
|
|
4
5
|
export declare function refCloseness(needle: string, candidate: string): number;
|
|
5
6
|
export declare function rankCandidates<T>(ref: string, rows: T[], keysOf: (row: T) => Array<string | null | undefined>, options?: {
|
package/dist/utils/id-hints.js
CHANGED
|
@@ -9,19 +9,44 @@
|
|
|
9
9
|
// returns only the recovery hint (no candidate ids).
|
|
10
10
|
export const HINT_SCAN_CAP = 200;
|
|
11
11
|
export const HINT_MAX_CANDIDATES = 5;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
12
|
+
// All supported hint keys (ids, slugs, hostnames, and numeric version refs) fit
|
|
13
|
+
// within this ceiling. Rejecting larger values keeps best-effort 404 recovery
|
|
14
|
+
// from turning attacker-controlled refs or stored labels into quadratic work.
|
|
15
|
+
export const HINT_MAX_KEY_LENGTH = 256;
|
|
16
|
+
function boundedLevenshtein(a, b, maxDistance) {
|
|
17
|
+
if (Math.abs(a.length - b.length) > maxDistance)
|
|
18
|
+
return maxDistance + 1;
|
|
19
|
+
// Keep the rolling rows on the shorter input. The band avoids evaluating
|
|
20
|
+
// cells that cannot contribute a result within maxDistance.
|
|
21
|
+
if (b.length > a.length)
|
|
22
|
+
[a, b] = [b, a];
|
|
23
|
+
let previous = new Uint16Array(b.length + 1);
|
|
24
|
+
let current = new Uint16Array(b.length + 1);
|
|
25
|
+
for (let j = 0; j <= b.length; j++)
|
|
26
|
+
previous[j] = j;
|
|
27
|
+
const outsideBand = maxDistance + 1;
|
|
28
|
+
for (let i = 1; i <= a.length; i++) {
|
|
29
|
+
current.fill(outsideBand);
|
|
30
|
+
current[0] = i <= maxDistance ? i : outsideBand;
|
|
31
|
+
const start = Math.max(1, i - maxDistance);
|
|
32
|
+
const end = Math.min(b.length, i + maxDistance);
|
|
33
|
+
let rowMinimum = current[0] ?? outsideBand;
|
|
34
|
+
for (let j = start; j <= end; j++) {
|
|
19
35
|
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
20
|
-
current[j] = Math.min((current[j - 1] ??
|
|
36
|
+
current[j] = Math.min((current[j - 1] ?? outsideBand) + 1, (previous[j] ?? outsideBand) + 1, (previous[j - 1] ?? outsideBand) + cost);
|
|
37
|
+
rowMinimum = Math.min(rowMinimum, current[j] ?? outsideBand);
|
|
21
38
|
}
|
|
22
|
-
|
|
39
|
+
if (rowMinimum > maxDistance)
|
|
40
|
+
return outsideBand;
|
|
41
|
+
[previous, current] = [current, previous];
|
|
42
|
+
}
|
|
43
|
+
return previous[b.length] ?? outsideBand;
|
|
44
|
+
}
|
|
45
|
+
export function levenshtein(a, b) {
|
|
46
|
+
if (a.length > HINT_MAX_KEY_LENGTH || b.length > HINT_MAX_KEY_LENGTH) {
|
|
47
|
+
return Number.POSITIVE_INFINITY;
|
|
23
48
|
}
|
|
24
|
-
return
|
|
49
|
+
return boundedLevenshtein(a, b, Math.max(a.length, b.length));
|
|
25
50
|
}
|
|
26
51
|
// Returns a small finite score the closer `candidate` is to `needle`, or
|
|
27
52
|
// +Infinity when they are not genuinely close. Exact match = 0; substring
|
|
@@ -29,14 +54,17 @@ export function levenshtein(a, b) {
|
|
|
29
54
|
export function refCloseness(needle, candidate) {
|
|
30
55
|
if (!candidate)
|
|
31
56
|
return Number.POSITIVE_INFINITY;
|
|
57
|
+
if (needle.length > HINT_MAX_KEY_LENGTH || candidate.length > HINT_MAX_KEY_LENGTH) {
|
|
58
|
+
return Number.POSITIVE_INFINITY;
|
|
59
|
+
}
|
|
32
60
|
if (candidate === needle)
|
|
33
61
|
return 0;
|
|
34
62
|
if (candidate.includes(needle) || needle.includes(candidate))
|
|
35
63
|
return 1;
|
|
36
|
-
const distance = levenshtein(needle, candidate);
|
|
37
64
|
// Only surface genuinely close typos: bound the allowed distance by the shorter
|
|
38
65
|
// string so short refs don't match everything.
|
|
39
66
|
const tolerance = Math.max(2, Math.floor(Math.min(needle.length, candidate.length) / 3));
|
|
67
|
+
const distance = boundedLevenshtein(needle, candidate, tolerance);
|
|
40
68
|
return distance <= tolerance ? 1 + distance : Number.POSITIVE_INFINITY;
|
|
41
69
|
}
|
|
42
70
|
// Rank `rows` by how close any of their keys are to `ref`, returning at most
|
|
@@ -44,14 +72,28 @@ export function refCloseness(needle, candidate) {
|
|
|
44
72
|
// only genuine near-matches surface. Ties break by the optional comparator
|
|
45
73
|
// (e.g. recency); keys are matched case-insensitively.
|
|
46
74
|
export function rankCandidates(ref, rows, keysOf, options) {
|
|
75
|
+
if (rows.length > HINT_SCAN_CAP || ref.length > HINT_MAX_KEY_LENGTH)
|
|
76
|
+
return [];
|
|
47
77
|
const needle = ref.toLowerCase();
|
|
78
|
+
// Unicode case conversion can expand a string (for example, U+0130). Keep
|
|
79
|
+
// that normalized representation under the same work ceiling too.
|
|
80
|
+
if (needle.length > HINT_MAX_KEY_LENGTH)
|
|
81
|
+
return [];
|
|
48
82
|
const max = options?.max ?? HINT_MAX_CANDIDATES;
|
|
49
83
|
return rows
|
|
50
84
|
.map((row) => {
|
|
51
|
-
|
|
85
|
+
let score = Number.POSITIVE_INFINITY;
|
|
86
|
+
for (const key of keysOf(row)) {
|
|
87
|
+
if (!key || key.length > HINT_MAX_KEY_LENGTH)
|
|
88
|
+
continue;
|
|
89
|
+
const candidate = key.toLowerCase();
|
|
90
|
+
if (candidate.length > HINT_MAX_KEY_LENGTH)
|
|
91
|
+
continue;
|
|
92
|
+
score = Math.min(score, refCloseness(needle, candidate));
|
|
93
|
+
}
|
|
52
94
|
return {
|
|
53
95
|
row,
|
|
54
|
-
score
|
|
96
|
+
score,
|
|
55
97
|
};
|
|
56
98
|
})
|
|
57
99
|
.filter((entry) => entry.score < Number.POSITIVE_INFINITY)
|
|
@@ -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;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { SpacePageTheme } from "../contracts/space-config.js";
|
|
2
|
+
/** Default page palette (mirrored into the shared template CSS). */
|
|
3
|
+
export declare const PAGE_COLOR_DEFAULTS: {
|
|
4
|
+
readonly accent: "#ff4217";
|
|
5
|
+
readonly bg: "#f9f8f4";
|
|
6
|
+
readonly fg: "#1a1913";
|
|
7
|
+
readonly muted: "#6f6b62";
|
|
8
|
+
readonly card: "#f9f8f4";
|
|
9
|
+
readonly border: "#ddd9cf";
|
|
10
|
+
readonly darkBg: "#141313";
|
|
11
|
+
readonly darkFg: "#e9e7e2";
|
|
12
|
+
readonly darkMuted: "#8f8b84";
|
|
13
|
+
readonly darkCard: "#141313";
|
|
14
|
+
readonly darkBorder: "#2b2a27";
|
|
15
|
+
};
|
|
16
|
+
export type PageSchemeColors = {
|
|
17
|
+
bg: string;
|
|
18
|
+
fg: string;
|
|
19
|
+
muted: string;
|
|
20
|
+
card: string;
|
|
21
|
+
border: string;
|
|
22
|
+
error: string;
|
|
23
|
+
};
|
|
24
|
+
export type DerivedPageColors = {
|
|
25
|
+
/** `adaptive` keeps the built-in light/dark schemes; otherwise pinned. */
|
|
26
|
+
scheme: "adaptive" | "light" | "dark";
|
|
27
|
+
/** The accent exactly as pushed (fills keep the brand color verbatim). */
|
|
28
|
+
accent: string;
|
|
29
|
+
/** Label color on accent fills. */
|
|
30
|
+
onAccent: string;
|
|
31
|
+
/** Accent-derived link color, readable on the light (or pinned) surface. */
|
|
32
|
+
link: string;
|
|
33
|
+
/** Adaptive only: link color readable on the built-in dark surface. */
|
|
34
|
+
linkDark?: string;
|
|
35
|
+
/** Full derived scheme when a background pins the page. */
|
|
36
|
+
pinned?: PageSchemeColors;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Return `color` unchanged when it already meets `target` on `surface`;
|
|
40
|
+
* otherwise move its OKLCH lightness (hue/chroma preserved) just far enough
|
|
41
|
+
* to meet it. Falls back to plain black/white when even full lightness range
|
|
42
|
+
* cannot reach the target (e.g. a mid-gray surface).
|
|
43
|
+
*/
|
|
44
|
+
export declare function readableOn(color: string, surface: string, target: number): string;
|
|
45
|
+
/** Button-label color for an accent fill: white when it can carry, else ink. */
|
|
46
|
+
export declare function onAccentColor(accent: string): string;
|
|
47
|
+
/** The one entry point: raw theme in, everything the templates paint with out. */
|
|
48
|
+
export declare function derivePageColors(theme: Pick<SpacePageTheme, "accent" | "background">): DerivedPageColors;
|
|
49
|
+
/** Hex form of any schema-accepted color, for color-picker inputs. */
|
|
50
|
+
export declare function pageColorToHex(value: string): string | null;
|