@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
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import { registeredIdSchema } from "./ids.js";
|
|
3
|
-
export const publishStates = [
|
|
4
|
-
"awaiting_upload",
|
|
5
|
-
"uploaded",
|
|
6
|
-
"parked",
|
|
7
|
-
"materializing",
|
|
8
|
-
"succeeded",
|
|
9
|
-
"failed",
|
|
10
|
-
"cancelled",
|
|
11
|
-
"expired",
|
|
12
|
-
];
|
|
13
|
-
export const publishStateSchema = z.enum(publishStates);
|
|
14
|
-
export const publishIdSchema = registeredIdSchema("pub");
|
|
15
|
-
const spaceIdSchema = registeredIdSchema("spc");
|
|
16
|
-
const versionIdSchema = registeredIdSchema("ver");
|
|
17
|
-
export const publishAdmissionSchema = z.object({
|
|
18
|
-
id: publishIdSchema,
|
|
19
|
-
idempotencyKey: z.string().min(1),
|
|
20
|
-
requestFingerprint: z.string().min(1),
|
|
21
|
-
});
|
|
22
|
-
const cancellableActionSchema = z.object({
|
|
23
|
-
kind: z.literal("cancel_publish"),
|
|
24
|
-
publishId: publishIdSchema,
|
|
25
|
-
});
|
|
26
|
-
export const publishSchema = z.discriminatedUnion("state", [
|
|
27
|
-
z
|
|
28
|
-
.object({
|
|
29
|
-
id: publishIdSchema,
|
|
30
|
-
state: z.literal("awaiting_upload"),
|
|
31
|
-
target: z
|
|
32
|
-
.object({
|
|
33
|
-
spaceId: spaceIdSchema,
|
|
34
|
-
baseVersionId: versionIdSchema.nullable(),
|
|
35
|
-
})
|
|
36
|
-
.optional(),
|
|
37
|
-
counts: z
|
|
38
|
-
.object({
|
|
39
|
-
received: z.number().int().nonnegative(),
|
|
40
|
-
missing: z.number().int().positive(),
|
|
41
|
-
})
|
|
42
|
-
.optional(),
|
|
43
|
-
expiresAt: z.string().datetime().optional(),
|
|
44
|
-
upload: z
|
|
45
|
-
.object({
|
|
46
|
-
missing: z
|
|
47
|
-
.array(z.object({
|
|
48
|
-
path: z.string(),
|
|
49
|
-
sha256: z
|
|
50
|
-
.string()
|
|
51
|
-
.regex(/^[0-9a-f]{64}$/)
|
|
52
|
-
.nullable(),
|
|
53
|
-
size: z.number().int().nonnegative(),
|
|
54
|
-
url: z.string().url(),
|
|
55
|
-
headers: z.record(z.string(), z.string()),
|
|
56
|
-
expiresAt: z.string().datetime(),
|
|
57
|
-
}))
|
|
58
|
-
.min(1),
|
|
59
|
-
})
|
|
60
|
-
.optional(),
|
|
61
|
-
})
|
|
62
|
-
.refine((publish) => publish.counts === undefined ||
|
|
63
|
-
publish.upload === undefined ||
|
|
64
|
-
publish.counts.missing === publish.upload.missing.length, {
|
|
65
|
-
message: "Upload instructions must match the missing upload count.",
|
|
66
|
-
path: ["upload", "missing"],
|
|
67
|
-
})
|
|
68
|
-
.refine((publish) => publish.counts !== undefined || publish.upload !== undefined, {
|
|
69
|
-
message: "An awaiting_upload publish must carry counts or upload instructions.",
|
|
70
|
-
path: ["counts"],
|
|
71
|
-
}),
|
|
72
|
-
z.object({ id: publishIdSchema, state: z.literal("uploaded") }),
|
|
73
|
-
z
|
|
74
|
-
.object({
|
|
75
|
-
id: publishIdSchema,
|
|
76
|
-
state: z.literal("parked"),
|
|
77
|
-
reason: z.literal("capacity"),
|
|
78
|
-
queuePosition: z.number().int().positive().optional(),
|
|
79
|
-
nextCheckAfterSeconds: z.number().int().positive(),
|
|
80
|
-
actions: z.tuple([cancellableActionSchema]),
|
|
81
|
-
})
|
|
82
|
-
.refine((publish) => publish.actions.every((action) => action.publishId === publish.id), {
|
|
83
|
-
message: "Cancel actions must reference the enclosing publish.",
|
|
84
|
-
path: ["actions"],
|
|
85
|
-
}),
|
|
86
|
-
z.object({
|
|
87
|
-
id: publishIdSchema,
|
|
88
|
-
state: z.literal("materializing"),
|
|
89
|
-
operation: z.object({
|
|
90
|
-
id: registeredIdSchema("op"),
|
|
91
|
-
kind: z.literal("publish.finalize"),
|
|
92
|
-
status: z.enum(["queued", "running"]),
|
|
93
|
-
}),
|
|
94
|
-
}),
|
|
95
|
-
z.object({
|
|
96
|
-
id: publishIdSchema,
|
|
97
|
-
state: z.literal("succeeded"),
|
|
98
|
-
outcome: z.enum(["created", "updated", "unchanged"]),
|
|
99
|
-
capturedBaseVersionId: versionIdSchema.nullable().optional(),
|
|
100
|
-
claim: z
|
|
101
|
-
.object({
|
|
102
|
-
token: z.string().min(1),
|
|
103
|
-
url: z.string().url(),
|
|
104
|
-
expiresAt: z.string().datetime(),
|
|
105
|
-
})
|
|
106
|
-
.optional(),
|
|
107
|
-
result: z.object({
|
|
108
|
-
space: z.object({ id: spaceIdSchema, liveUrl: z.string().url() }),
|
|
109
|
-
version: z.object({ id: versionIdSchema, immutableUrl: z.string().url() }),
|
|
110
|
-
live: z.object({
|
|
111
|
-
previousVersionId: versionIdSchema.nullable(),
|
|
112
|
-
versionId: versionIdSchema,
|
|
113
|
-
}),
|
|
114
|
-
}),
|
|
115
|
-
}),
|
|
116
|
-
z.object({ id: publishIdSchema, state: z.literal("failed"), errorCode: z.string().min(1) }),
|
|
117
|
-
z.object({ id: publishIdSchema, state: z.literal("cancelled") }),
|
|
118
|
-
z.object({ id: publishIdSchema, state: z.literal("expired") }),
|
|
119
|
-
]);
|
|
120
|
-
// Cancellation may win during materialization only before the Version exists;
|
|
121
|
-
// afterwards it is not cancelable and the outcome is unchanged.
|
|
122
|
-
const allowedTransitions = {
|
|
123
|
-
awaiting_upload: ["uploaded", "cancelled", "expired"],
|
|
124
|
-
uploaded: ["parked", "materializing", "cancelled", "expired"],
|
|
125
|
-
parked: ["materializing", "cancelled", "expired"],
|
|
126
|
-
materializing: ["succeeded", "failed", "cancelled", "expired"],
|
|
127
|
-
succeeded: [],
|
|
128
|
-
failed: [],
|
|
129
|
-
cancelled: [],
|
|
130
|
-
expired: [],
|
|
131
|
-
};
|
|
132
|
-
export function canTransitionPublish(from, to) {
|
|
133
|
-
return allowedTransitions[from].some((candidate) => candidate === to);
|
|
134
|
-
}
|
|
135
|
-
export function transitionPublish(publish, next) {
|
|
136
|
-
if (publish.id !== next.id ||
|
|
137
|
-
publish.idempotencyKey !== next.idempotencyKey ||
|
|
138
|
-
publish.requestFingerprint !== next.requestFingerprint) {
|
|
139
|
-
throw new Error("A publish transition cannot change the durable publish identity.");
|
|
140
|
-
}
|
|
141
|
-
if (!canTransitionPublish(publish.state, next.state)) {
|
|
142
|
-
throw new Error(`Invalid publish transition: ${publish.state} -> ${next.state}`);
|
|
143
|
-
}
|
|
144
|
-
// Project through the wire schema so fields from the previous variant (e.g. a
|
|
145
|
-
// parked `reason` spread into a materializing publish) never leak downstream.
|
|
146
|
-
return {
|
|
147
|
-
...publishSchema.parse(next),
|
|
148
|
-
idempotencyKey: next.idempotencyKey,
|
|
149
|
-
requestFingerprint: next.requestFingerprint,
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
export function convergePublishAdmission(existing, attempted) {
|
|
153
|
-
if (existing === undefined)
|
|
154
|
-
return { outcome: "created", publish: attempted };
|
|
155
|
-
if (existing.idempotencyKey !== attempted.idempotencyKey) {
|
|
156
|
-
throw new Error("Admission convergence requires one idempotency key.");
|
|
157
|
-
}
|
|
158
|
-
if (existing.requestFingerprint !== attempted.requestFingerprint) {
|
|
159
|
-
return { outcome: "conflict", publish: existing };
|
|
160
|
-
}
|
|
161
|
-
return { outcome: "replayed", publish: existing };
|
|
162
|
-
}
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
export declare function filterGrantsByNamespace(grants: string[], namespaces: string[]): string[];
|
|
2
|
-
export declare function grantGlobMatches(pattern: string, grant: string): boolean;
|
|
3
|
-
export declare function grantsIntersect(grants: string[], requiredGrants: string[]): boolean;
|
|
4
|
-
export declare function grantsSatisfyRule(input: {
|
|
5
|
-
grants: string[];
|
|
6
|
-
requiredGrants: string[];
|
|
7
|
-
grantNamespaces: string[];
|
|
8
|
-
}): boolean;
|
|
9
|
-
export declare function canonicalizeHost(host: string): string;
|
|
10
|
-
export declare function hostGlobMatches(pattern: string, host: string): boolean;
|
|
11
|
-
export declare function hostTemplateMatches(template: string, host: string): boolean;
|
|
12
|
-
export declare function pathGlobMatches(pattern: string, path: string): boolean;
|
|
13
|
-
export declare function agentMatches(needle: string, agent: string): boolean;
|
|
14
|
-
export declare function ipInCidr(ip: string, cidr: string): boolean;
|
|
15
|
-
export declare function ipInAnyCidr(ip: string, cidrs: string[]): boolean;
|
|
16
|
-
export type RuleMatchContext = {
|
|
17
|
-
host: string;
|
|
18
|
-
path: string;
|
|
19
|
-
channel?: string | undefined;
|
|
20
|
-
ip?: string | undefined;
|
|
21
|
-
agent?: string | undefined;
|
|
22
|
-
country?: string | undefined;
|
|
23
|
-
headers?: Record<string, string> | undefined;
|
|
24
|
-
};
|
|
25
|
-
export type RuleMatchInput = {
|
|
26
|
-
host?: string | undefined;
|
|
27
|
-
hostPattern?: string | undefined;
|
|
28
|
-
hostTemplate?: string | undefined;
|
|
29
|
-
pathPattern?: string | undefined;
|
|
30
|
-
channel?: string | undefined;
|
|
31
|
-
ipCidrs?: string[] | undefined;
|
|
32
|
-
agent?: string | undefined;
|
|
33
|
-
country?: string | undefined;
|
|
34
|
-
header?: {
|
|
35
|
-
name: string;
|
|
36
|
-
value: string;
|
|
37
|
-
} | undefined;
|
|
38
|
-
};
|
|
39
|
-
export declare function ruleMatchApplies(match: RuleMatchInput, context: RuleMatchContext): boolean;
|
|
@@ -1,296 +0,0 @@
|
|
|
1
|
-
// The ONE TS implementation of the runtime's grant-satisfaction kernel
|
|
2
|
-
// (internal-docs/access-plan.html X-9): glob-aware `grants ∩ requiredGrants`
|
|
3
|
-
// intersection plus signing-key grant-namespace filtering. The PHP evaluator
|
|
4
|
-
// (runtime/engine/runtime/access-rules.php `_stattic_unified_grants_intersect`
|
|
5
|
-
// / `_stattic_unified_filter_grants_by_namespace`) is the enforcer of record;
|
|
6
|
-
// this matcher exists so the cloud's authorize pre-flight (and, later,
|
|
7
|
-
// `access simulate`) never mints a token the runtime would reject.
|
|
8
|
-
//
|
|
9
|
-
// Parity is pinned by a shared JSON corpus (`access-match.fixtures.json`),
|
|
10
|
-
// asserted against this module in packages/common and against the PHP in the
|
|
11
|
-
// control plane's php-policy-parity test — the one sanctioned
|
|
12
|
-
// generated↔source drift-guard pattern. Do not change semantics here without
|
|
13
|
-
// extending the corpus.
|
|
14
|
-
// Drops every grant not prefixed by one of the signing key's namespaces. This
|
|
15
|
-
// bounds a leaked external key's blast radius to exactly the grant namespaces
|
|
16
|
-
// the owner pointed it at (and bounds the runtime-local pw: key to `pw:`).
|
|
17
|
-
export function filterGrantsByNamespace(grants, namespaces) {
|
|
18
|
-
const kept = [];
|
|
19
|
-
for (const grant of grants) {
|
|
20
|
-
if (typeof grant !== "string" || grant === "") {
|
|
21
|
-
continue;
|
|
22
|
-
}
|
|
23
|
-
for (const namespace of namespaces) {
|
|
24
|
-
if (typeof namespace === "string" && namespace !== "" && grant.startsWith(namespace)) {
|
|
25
|
-
kept.push(grant);
|
|
26
|
-
break;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
return kept;
|
|
31
|
-
}
|
|
32
|
-
// `*` in a requiredGrant matches any run of characters (grants have no path
|
|
33
|
-
// structure to anchor on, unlike paths). Exact match when no `*` present.
|
|
34
|
-
export function grantGlobMatches(pattern, grant) {
|
|
35
|
-
if (!pattern.includes("*")) {
|
|
36
|
-
return pattern === grant;
|
|
37
|
-
}
|
|
38
|
-
const regex = pattern
|
|
39
|
-
.split("*")
|
|
40
|
-
.map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
|
|
41
|
-
.join(".*");
|
|
42
|
-
return new RegExp(`^${regex}$`).test(grant);
|
|
43
|
-
}
|
|
44
|
-
// Glob-aware intersection: a visitor passes when ANY verified concrete grant
|
|
45
|
-
// matches ANY requiredGrant. requiredGrants may carry `*` wildcards (e.g.
|
|
46
|
-
// "email:*@acme.com"); verified grants are concrete.
|
|
47
|
-
export function grantsIntersect(grants, requiredGrants) {
|
|
48
|
-
for (const required of requiredGrants) {
|
|
49
|
-
if (typeof required !== "string" || required === "") {
|
|
50
|
-
continue;
|
|
51
|
-
}
|
|
52
|
-
for (const grant of grants) {
|
|
53
|
-
if (typeof grant !== "string" || grant === "") {
|
|
54
|
-
continue;
|
|
55
|
-
}
|
|
56
|
-
if (grantGlobMatches(required, grant)) {
|
|
57
|
-
return true;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
return false;
|
|
62
|
-
}
|
|
63
|
-
// The composed satisfaction test the authorize pre-flight runs: namespace-drop
|
|
64
|
-
// the visitor's grants by the signing key, then intersect with the rule's
|
|
65
|
-
// requiredGrants — exactly what the runtime does after verifying a signature.
|
|
66
|
-
export function grantsSatisfyRule(input) {
|
|
67
|
-
return grantsIntersect(filterGrantsByNamespace(input.grants, input.grantNamespaces), input.requiredGrants);
|
|
68
|
-
}
|
|
69
|
-
// ---------------------------------------------------------------------------
|
|
70
|
-
// Rule-match evaluation (the `match` half of the kernel)
|
|
71
|
-
// ---------------------------------------------------------------------------
|
|
72
|
-
//
|
|
73
|
-
// TS mirror of the PHP `_stattic_unified_rule_matches` /
|
|
74
|
-
// `_stattic_unified_path_glob_matches` / `_stattic_unified_ip_in_cidr` family
|
|
75
|
-
// (runtime/engine/runtime/access-rules.php). Powers `access simulate` and the
|
|
76
|
-
// authorize pre-flight's path scoping. Parity is pinned by the same shared
|
|
77
|
-
// corpus (`access-match.fixtures.json`, sections `pathGlob` + `cidr`) asserted
|
|
78
|
-
// against the PHP in php-policy-parity.test.ts.
|
|
79
|
-
// Mirrors `_spacefast_canonicalize_host`.
|
|
80
|
-
export function canonicalizeHost(host) {
|
|
81
|
-
return host.trim().toLowerCase().replace(/\.+$/, "");
|
|
82
|
-
}
|
|
83
|
-
export function hostGlobMatches(pattern, host) {
|
|
84
|
-
const regex = pattern
|
|
85
|
-
.split("*")
|
|
86
|
-
.map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
|
|
87
|
-
.join(".*");
|
|
88
|
-
return new RegExp(`^${regex}$`, "i").test(canonicalizeHost(host));
|
|
89
|
-
}
|
|
90
|
-
export function hostTemplateMatches(template, host) {
|
|
91
|
-
let regex = "";
|
|
92
|
-
let index = 0;
|
|
93
|
-
while (index < template.length) {
|
|
94
|
-
const char = template[index];
|
|
95
|
-
if (char === "*") {
|
|
96
|
-
regex += ".*";
|
|
97
|
-
index += 1;
|
|
98
|
-
continue;
|
|
99
|
-
}
|
|
100
|
-
if (char === ":") {
|
|
101
|
-
const name = template.slice(index + 1).match(/^[A-Za-z][A-Za-z0-9_]*/)?.[0] ?? "";
|
|
102
|
-
if (!name) {
|
|
103
|
-
return false;
|
|
104
|
-
}
|
|
105
|
-
regex += "[^.]+";
|
|
106
|
-
index += name.length + 1;
|
|
107
|
-
continue;
|
|
108
|
-
}
|
|
109
|
-
regex += char.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
110
|
-
index += 1;
|
|
111
|
-
}
|
|
112
|
-
return new RegExp(`^${regex}$`, "i").test(canonicalizeHost(host));
|
|
113
|
-
}
|
|
114
|
-
// Glob path matcher: "/docs/**" (any depth), "/docs/*" (one segment), exact
|
|
115
|
-
// otherwise. Empty pattern matches everything.
|
|
116
|
-
export function pathGlobMatches(pattern, path) {
|
|
117
|
-
if (pattern === "") {
|
|
118
|
-
return true;
|
|
119
|
-
}
|
|
120
|
-
if (!pattern.includes("*")) {
|
|
121
|
-
return pattern === path;
|
|
122
|
-
}
|
|
123
|
-
let regex = "";
|
|
124
|
-
let i = 0;
|
|
125
|
-
while (i < pattern.length) {
|
|
126
|
-
const char = pattern[i];
|
|
127
|
-
if (char === "*") {
|
|
128
|
-
if (pattern[i + 1] === "*") {
|
|
129
|
-
regex += ".*";
|
|
130
|
-
i += 2;
|
|
131
|
-
continue;
|
|
132
|
-
}
|
|
133
|
-
regex += "[^/]*";
|
|
134
|
-
i += 1;
|
|
135
|
-
continue;
|
|
136
|
-
}
|
|
137
|
-
regex += char.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
138
|
-
i += 1;
|
|
139
|
-
}
|
|
140
|
-
return new RegExp(`^${regex}$`).test(path);
|
|
141
|
-
}
|
|
142
|
-
// Case-insensitive substring match on the user agent; empty needle matches.
|
|
143
|
-
export function agentMatches(needle, agent) {
|
|
144
|
-
if (needle === "") {
|
|
145
|
-
return true;
|
|
146
|
-
}
|
|
147
|
-
return agent.toLowerCase().includes(needle.toLowerCase());
|
|
148
|
-
}
|
|
149
|
-
// inet_pton equivalent: dotted-quad IPv4 or RFC 4291 IPv6 (including "::"
|
|
150
|
-
// compression and an embedded IPv4 tail) → bytes, or null when unparseable.
|
|
151
|
-
function ipToBytes(ip) {
|
|
152
|
-
if (/^\d{1,3}(\.\d{1,3}){3}$/.test(ip)) {
|
|
153
|
-
const parts = ip.split(".").map(Number);
|
|
154
|
-
if (parts.some((part) => part > 255)) {
|
|
155
|
-
return null;
|
|
156
|
-
}
|
|
157
|
-
return Uint8Array.from(parts);
|
|
158
|
-
}
|
|
159
|
-
if (!ip.includes(":")) {
|
|
160
|
-
return null;
|
|
161
|
-
}
|
|
162
|
-
let head = ip;
|
|
163
|
-
let v4Tail = null;
|
|
164
|
-
const lastColon = ip.lastIndexOf(":");
|
|
165
|
-
const tail = ip.slice(lastColon + 1);
|
|
166
|
-
if (tail.includes(".")) {
|
|
167
|
-
const tailBytes = ipToBytes(tail);
|
|
168
|
-
if (!tailBytes || tailBytes.length !== 4) {
|
|
169
|
-
return null;
|
|
170
|
-
}
|
|
171
|
-
v4Tail = [...tailBytes];
|
|
172
|
-
head = ip.slice(0, lastColon + 1) + "0:0"; // placeholder groups, replaced below
|
|
173
|
-
}
|
|
174
|
-
const doubleColonSplits = head.split("::");
|
|
175
|
-
if (doubleColonSplits.length > 2) {
|
|
176
|
-
return null;
|
|
177
|
-
}
|
|
178
|
-
const parseGroups = (raw) => {
|
|
179
|
-
if (raw === "") {
|
|
180
|
-
return [];
|
|
181
|
-
}
|
|
182
|
-
const groups = [];
|
|
183
|
-
for (const part of raw.split(":")) {
|
|
184
|
-
if (!/^[0-9a-fA-F]{1,4}$/.test(part)) {
|
|
185
|
-
return null;
|
|
186
|
-
}
|
|
187
|
-
groups.push(Number.parseInt(part, 16));
|
|
188
|
-
}
|
|
189
|
-
return groups;
|
|
190
|
-
};
|
|
191
|
-
let groups;
|
|
192
|
-
if (doubleColonSplits.length === 2) {
|
|
193
|
-
const left = parseGroups(doubleColonSplits[0] ?? "");
|
|
194
|
-
const right = parseGroups(doubleColonSplits[1] ?? "");
|
|
195
|
-
if (left === null || right === null || left.length + right.length > 8) {
|
|
196
|
-
return null;
|
|
197
|
-
}
|
|
198
|
-
groups = [...left, ...Array(8 - left.length - right.length).fill(0), ...right];
|
|
199
|
-
}
|
|
200
|
-
else {
|
|
201
|
-
groups = parseGroups(head);
|
|
202
|
-
if (groups === null || groups.length !== 8) {
|
|
203
|
-
return null;
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
const bytes = new Uint8Array(16);
|
|
207
|
-
groups.forEach((group, index) => {
|
|
208
|
-
bytes[index * 2] = group >> 8;
|
|
209
|
-
bytes[index * 2 + 1] = group & 0xff;
|
|
210
|
-
});
|
|
211
|
-
if (v4Tail) {
|
|
212
|
-
bytes[12] = v4Tail[0];
|
|
213
|
-
bytes[13] = v4Tail[1];
|
|
214
|
-
bytes[14] = v4Tail[2];
|
|
215
|
-
bytes[15] = v4Tail[3];
|
|
216
|
-
}
|
|
217
|
-
return bytes;
|
|
218
|
-
}
|
|
219
|
-
// Mirrors `_stattic_unified_ip_in_cidr`: a bare address (no "/") is an exact
|
|
220
|
-
// string compare; mixed families never match; malformed input never matches.
|
|
221
|
-
export function ipInCidr(ip, cidr) {
|
|
222
|
-
if (cidr === "") {
|
|
223
|
-
return false;
|
|
224
|
-
}
|
|
225
|
-
if (!cidr.includes("/")) {
|
|
226
|
-
return ip === cidr;
|
|
227
|
-
}
|
|
228
|
-
const [subnet = "", bitsRaw = ""] = cidr.split("/", 2);
|
|
229
|
-
const ipBytes = ipToBytes(ip);
|
|
230
|
-
const subnetBytes = ipToBytes(subnet);
|
|
231
|
-
if (!ipBytes || !subnetBytes || ipBytes.length !== subnetBytes.length) {
|
|
232
|
-
return false;
|
|
233
|
-
}
|
|
234
|
-
if (!/^\d+$/.test(bitsRaw)) {
|
|
235
|
-
return false;
|
|
236
|
-
}
|
|
237
|
-
const bits = Number.parseInt(bitsRaw, 10);
|
|
238
|
-
const maxBits = ipBytes.length * 8;
|
|
239
|
-
if (bits < 0 || bits > maxBits) {
|
|
240
|
-
return false;
|
|
241
|
-
}
|
|
242
|
-
const fullBytes = Math.floor(bits / 8);
|
|
243
|
-
for (let index = 0; index < fullBytes; index += 1) {
|
|
244
|
-
if (ipBytes[index] !== subnetBytes[index]) {
|
|
245
|
-
return false;
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
const remainder = bits % 8;
|
|
249
|
-
if (remainder === 0) {
|
|
250
|
-
return true;
|
|
251
|
-
}
|
|
252
|
-
const mask = (0xff << (8 - remainder)) & 0xff;
|
|
253
|
-
return (ipBytes[fullBytes] & mask) === (subnetBytes[fullBytes] & mask);
|
|
254
|
-
}
|
|
255
|
-
export function ipInAnyCidr(ip, cidrs) {
|
|
256
|
-
if (ip === "") {
|
|
257
|
-
return false;
|
|
258
|
-
}
|
|
259
|
-
return cidrs.some((cidr) => typeof cidr === "string" && ipInCidr(ip, cidr));
|
|
260
|
-
}
|
|
261
|
-
// AND-combined match — mirrors `_stattic_unified_rule_matches` exactly. An
|
|
262
|
-
// empty match matches everything in scope.
|
|
263
|
-
export function ruleMatchApplies(match, context) {
|
|
264
|
-
if (match.host !== undefined && canonicalizeHost(match.host) !== canonicalizeHost(context.host)) {
|
|
265
|
-
return false;
|
|
266
|
-
}
|
|
267
|
-
if (match.hostPattern !== undefined && !hostGlobMatches(match.hostPattern, context.host)) {
|
|
268
|
-
return false;
|
|
269
|
-
}
|
|
270
|
-
if (match.hostTemplate !== undefined && !hostTemplateMatches(match.hostTemplate, context.host)) {
|
|
271
|
-
return false;
|
|
272
|
-
}
|
|
273
|
-
if (match.pathPattern !== undefined && !pathGlobMatches(match.pathPattern, context.path)) {
|
|
274
|
-
return false;
|
|
275
|
-
}
|
|
276
|
-
if (match.channel !== undefined && match.channel !== (context.channel ?? "")) {
|
|
277
|
-
return false;
|
|
278
|
-
}
|
|
279
|
-
if (match.ipCidrs !== undefined && !ipInAnyCidr(context.ip ?? "", match.ipCidrs)) {
|
|
280
|
-
return false;
|
|
281
|
-
}
|
|
282
|
-
if (match.agent !== undefined && !agentMatches(match.agent, context.agent ?? "")) {
|
|
283
|
-
return false;
|
|
284
|
-
}
|
|
285
|
-
if (match.country !== undefined && match.country.toUpperCase() !== (context.country ?? "")) {
|
|
286
|
-
return false;
|
|
287
|
-
}
|
|
288
|
-
if (match.header !== undefined) {
|
|
289
|
-
const name = match.header.name.toLowerCase();
|
|
290
|
-
const actual = Object.entries(context.headers ?? {}).find(([key]) => key.toLowerCase() === name)?.[1];
|
|
291
|
-
if (match.header.name === "" || (actual ?? "") !== match.header.value) {
|
|
292
|
-
return false;
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
return true;
|
|
296
|
-
}
|