@void/platform 0.20.1 → 0.20.2
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/README.md +10 -1
- package/dist/email-protocol.d.mts +48 -0
- package/dist/email-protocol.mjs +278 -0
- package/dist/index.d.mts +152 -13
- package/dist/index.mjs +346 -20
- package/dist/installer.d.mts +4 -2
- package/dist/installer.mjs +13 -4
- package/dist/runtime/api/README.md +1 -1
- package/dist/runtime/api/index.js +67382 -54178
- package/dist/runtime/api/index.js.map +4 -4
- package/dist/runtime/dispatch/README.md +1 -1
- package/dist/runtime/dispatch/index.js +26 -8
- package/dist/runtime/dispatch/index.js.map +2 -2
- package/dist/runtime/drizzle/0041_military_beyonder.sql +119 -0
- package/dist/runtime/drizzle/0042_dark_the_anarchist.sql +1 -0
- package/dist/runtime/drizzle/0043_nostalgic_meggan.sql +21 -0
- package/dist/runtime/drizzle/0044_smart_mentor.sql +15 -0
- package/dist/runtime/drizzle/0045_platform_settings.sql +5 -0
- package/dist/runtime/drizzle/0046_email_domains_platform_credential.sql +8 -0
- package/dist/runtime/drizzle/0047_email_gateway.sql +226 -0
- package/dist/runtime/drizzle/0048_project_teams.sql +133 -0
- package/dist/runtime/drizzle/compatibility.json +4 -0
- package/dist/runtime/drizzle/meta/_journal.json +56 -0
- package/dist/runtime/emailGateway/README.md +1 -0
- package/dist/runtime/emailGateway/index.js +5893 -0
- package/dist/runtime/emailGateway/index.js.map +8 -0
- package/dist/runtime/manifest.json +70 -11
- package/dist/runtime/proxy/README.md +1 -1
- package/dist/runtime/proxy/index.js +1629 -2242
- package/dist/runtime/proxy/index.js.map +4 -4
- package/dist/runtime/tail/README.md +1 -1
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -16,12 +16,19 @@ for administrator sessions and audit records. See
|
|
|
16
16
|
[Platform Administration](../../docs/guide/platform-administration.md) to operate
|
|
17
17
|
an installed platform from your terminal.
|
|
18
18
|
|
|
19
|
+
Platforms can use GitHub, Google, generic OIDC, Cloudflare Access, or a combination.
|
|
20
|
+
Access protection is independent from login methods and can admit company-approved
|
|
21
|
+
users automatically. Login settings, explicit account linking, and provider recovery
|
|
22
|
+
are available through Void; upgrades preserve live settings and reject runtimes
|
|
23
|
+
that cannot enforce their session revocation. See the installation and administration
|
|
24
|
+
guides for setup, scoped CI credentials, and recovery requirements.
|
|
25
|
+
|
|
19
26
|
The package contains:
|
|
20
27
|
|
|
21
28
|
- versioned deployment and installation manifests
|
|
22
29
|
- deterministic resource naming and application-route generation
|
|
23
30
|
- discovery-document and capability contracts
|
|
24
|
-
- Cloudflare Worker configuration generation for the
|
|
31
|
+
- Cloudflare Worker configuration generation for the API, proxy, tail, dispatch, and email gateway Workers
|
|
25
32
|
- A shared installation-owned traffic-disable protocol used by lifecycle-safe retained Workers
|
|
26
33
|
- integrity-checked, deployable Worker bundles plus a schema-versioned PostgreSQL migration and rollback-compatibility manifest under the `./installer` export
|
|
27
34
|
|
|
@@ -31,6 +38,8 @@ Interactive lifecycle operations authenticate the human operator through Cloudfl
|
|
|
31
38
|
|
|
32
39
|
Fresh installations require a unique JWT signing secret with at least 32 bytes of random material and a unique, canonical base64-encoded 32-byte project-encryption key from the operator's durable secret manager. The installer never makes an ephemeral workstation or CI registry the only custodian of these installation identity keys. Existing Workers preserve their deployed values during routine lifecycle operations.
|
|
33
40
|
|
|
41
|
+
Email-enabled installations also need an independent email signing key. The installer generates it in encrypted recovery state, or accepts `VOID_PLATFORM_EMAIL_SIGNING_SECRET` from the operator's secret manager. Keep that original value for recovery on another machine and for headless upgrades without persistent recovery files.
|
|
42
|
+
|
|
34
43
|
Browser OAuth cannot manage AI Gateway. Installation previews mark an inaccessible gateway as requiring inspection; apply rechecks it with the already-required runtime token (AI Gateway: Edit) before provisioning. All other provider operations keep the operator credential. Later OAuth lifecycle operations can reuse that token from encrypted local recovery storage. On another workstation, use a management API token with AI Gateway access or supply the runtime token explicitly for that check.
|
|
35
44
|
|
|
36
45
|
Runtime and installation manifests record the last usage protocol proven live across the complete Worker set. A protocol transition gates traffic before the first Worker update and opens it only after target health and immutable versions pass; interrupted transitions remain gated and resumable.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
//#region src/email-protocol.d.ts
|
|
2
|
+
/** Worker-safe wire contract shared by the platform gateway and tenant entry. */
|
|
3
|
+
export declare const EMAIL_PROTOCOL_VERSION = 1;
|
|
4
|
+
export declare const EMAIL_RECEIPT_PATH = "/__void/email/receipt/v1";
|
|
5
|
+
export declare const EMAIL_MAX_ACTIONS: number;
|
|
6
|
+
export declare const EMAIL_MAX_METADATA_BYTES: number;
|
|
7
|
+
export declare const EMAIL_MAX_RESPONSE_BYTES: number;
|
|
8
|
+
export declare const EMAIL_MAX_REPLY_BYTES: number;
|
|
9
|
+
export declare const EMAIL_MAX_INBOUND_BYTES: number;
|
|
10
|
+
export declare const EMAIL_MAX_INBOUND_HEADERS: number;
|
|
11
|
+
export declare const EMAIL_MAX_INBOUND_HEADER_BYTES: number;
|
|
12
|
+
export type EmailWireAction = {
|
|
13
|
+
type: "reject";
|
|
14
|
+
reason: string;
|
|
15
|
+
} | {
|
|
16
|
+
type: "forward";
|
|
17
|
+
to: string;
|
|
18
|
+
headers?: Record<string, string>;
|
|
19
|
+
} | {
|
|
20
|
+
type: "reply";
|
|
21
|
+
from: string;
|
|
22
|
+
to: string;
|
|
23
|
+
raw: Uint8Array;
|
|
24
|
+
};
|
|
25
|
+
export type EmailReceiptClaims = {
|
|
26
|
+
version: 1;
|
|
27
|
+
receiptId: string;
|
|
28
|
+
projectId: string;
|
|
29
|
+
deploymentId: string;
|
|
30
|
+
recipient: string;
|
|
31
|
+
expiresAt: number;
|
|
32
|
+
};
|
|
33
|
+
export type EmailWireEnvelope = {
|
|
34
|
+
from: string;
|
|
35
|
+
to: string;
|
|
36
|
+
headers: Record<string, string>;
|
|
37
|
+
raw: Uint8Array;
|
|
38
|
+
rawSize: number;
|
|
39
|
+
viaDomain?: string;
|
|
40
|
+
};
|
|
41
|
+
export declare function encodeEmailEnvelope(envelope: EmailWireEnvelope): Uint8Array;
|
|
42
|
+
export declare function readEmailEnvelope(stream: ReadableStream<Uint8Array> | null, deadlineMs?: number): Promise<EmailWireEnvelope>;
|
|
43
|
+
export declare function signEmailReceipt(claims: EmailReceiptClaims, signingSecret: string): Promise<string>;
|
|
44
|
+
export declare function verifyEmailReceipt(token: string, signingSecret: string, expected: Pick<EmailReceiptClaims, "projectId" | "deploymentId" | "recipient">, now?: number): Promise<EmailReceiptClaims | null>;
|
|
45
|
+
export declare function encodeEmailActions(actions: ReadonlyArray<EmailWireAction>): ReadableStream<Uint8Array>;
|
|
46
|
+
/** Read each frame with fixed ceilings before parsing metadata or allocating a reply. */
|
|
47
|
+
export declare function readEmailActions(stream: ReadableStream<Uint8Array>, deadlineMs?: number): Promise<Array<EmailWireAction>>;
|
|
48
|
+
//#endregion
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
//#region src/email-protocol.ts
|
|
2
|
+
/** Worker-safe wire contract shared by the platform gateway and tenant entry. */
|
|
3
|
+
const EMAIL_PROTOCOL_VERSION = 1;
|
|
4
|
+
const EMAIL_RECEIPT_PATH = "/__void/email/receipt/v1";
|
|
5
|
+
const EMAIL_MAX_ACTIONS = 100;
|
|
6
|
+
const EMAIL_MAX_METADATA_BYTES = 8192;
|
|
7
|
+
const EMAIL_MAX_RESPONSE_BYTES = 8388608;
|
|
8
|
+
const EMAIL_MAX_REPLY_BYTES = 5242880;
|
|
9
|
+
const EMAIL_MAX_INBOUND_BYTES = 10485760;
|
|
10
|
+
const EMAIL_MAX_INBOUND_HEADERS = 1024;
|
|
11
|
+
const EMAIL_MAX_INBOUND_HEADER_BYTES = 131072;
|
|
12
|
+
const encoder = new TextEncoder();
|
|
13
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
14
|
+
function assertFlatJson(bytes, maxDepth, maxFields) {
|
|
15
|
+
let depth = 0;
|
|
16
|
+
let fields = 0;
|
|
17
|
+
let quoted = false;
|
|
18
|
+
let escaped = false;
|
|
19
|
+
for (const byte of bytes) if (quoted) {
|
|
20
|
+
if (escaped) escaped = false;
|
|
21
|
+
else if (byte === 92) escaped = true;
|
|
22
|
+
else if (byte === 34) quoted = false;
|
|
23
|
+
} else if (byte === 34) quoted = true;
|
|
24
|
+
else if (byte === 123 || byte === 91) {
|
|
25
|
+
if (++depth > maxDepth) throw new Error("email metadata too deep");
|
|
26
|
+
} else if (byte === 125 || byte === 93) {
|
|
27
|
+
if (--depth < 0) throw new Error("invalid email metadata");
|
|
28
|
+
} else if (byte === 58 && ++fields > maxFields) throw new Error("too many email metadata fields");
|
|
29
|
+
if (quoted || depth !== 0) throw new Error("invalid email metadata");
|
|
30
|
+
}
|
|
31
|
+
function encodeEmailEnvelope(envelope) {
|
|
32
|
+
const metadata = encoder.encode(JSON.stringify({
|
|
33
|
+
from: envelope.from,
|
|
34
|
+
to: envelope.to,
|
|
35
|
+
headers: envelope.headers,
|
|
36
|
+
rawSize: envelope.rawSize,
|
|
37
|
+
...envelope.viaDomain ? { viaDomain: envelope.viaDomain } : {}
|
|
38
|
+
}));
|
|
39
|
+
if (metadata.byteLength > 131072 || envelope.raw.byteLength > 10485760) throw new Error("email envelope too large");
|
|
40
|
+
const bytes = new Uint8Array(4 + metadata.byteLength + envelope.raw.byteLength);
|
|
41
|
+
new DataView(bytes.buffer).setUint32(0, metadata.byteLength);
|
|
42
|
+
bytes.set(metadata, 4);
|
|
43
|
+
bytes.set(envelope.raw, 4 + metadata.byteLength);
|
|
44
|
+
return bytes;
|
|
45
|
+
}
|
|
46
|
+
async function readEmailEnvelope(stream, deadlineMs = 15e3) {
|
|
47
|
+
if (!stream) throw new Error("missing email envelope");
|
|
48
|
+
const reader = stream.getReader();
|
|
49
|
+
const limit = 10616836;
|
|
50
|
+
let bytes = /* @__PURE__ */ new Uint8Array(1024);
|
|
51
|
+
let total = 0;
|
|
52
|
+
const deadline = Date.now() + deadlineMs;
|
|
53
|
+
try {
|
|
54
|
+
for (;;) {
|
|
55
|
+
const remaining = deadline - Date.now();
|
|
56
|
+
if (remaining <= 0) throw new Error("email envelope timeout");
|
|
57
|
+
let timer;
|
|
58
|
+
const chunk = await Promise.race([reader.read(), new Promise((_, reject) => {
|
|
59
|
+
timer = setTimeout(() => reject(/* @__PURE__ */ new Error("email envelope timeout")), remaining);
|
|
60
|
+
})]).finally(() => {
|
|
61
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
62
|
+
});
|
|
63
|
+
if (chunk.done) break;
|
|
64
|
+
const nextTotal = total + chunk.value.byteLength;
|
|
65
|
+
if (nextTotal > limit) throw new Error("email envelope too large");
|
|
66
|
+
if (nextTotal > bytes.byteLength) {
|
|
67
|
+
let capacity = bytes.byteLength;
|
|
68
|
+
while (capacity < nextTotal) capacity = Math.min(limit, capacity * 2);
|
|
69
|
+
const grown = new Uint8Array(capacity);
|
|
70
|
+
grown.set(bytes.subarray(0, total));
|
|
71
|
+
bytes = grown;
|
|
72
|
+
}
|
|
73
|
+
bytes.set(chunk.value, total);
|
|
74
|
+
total = nextTotal;
|
|
75
|
+
}
|
|
76
|
+
} catch (error) {
|
|
77
|
+
reader.cancel().catch(() => {});
|
|
78
|
+
throw error;
|
|
79
|
+
} finally {
|
|
80
|
+
try {
|
|
81
|
+
reader.releaseLock();
|
|
82
|
+
} catch {}
|
|
83
|
+
}
|
|
84
|
+
if (total < 4) throw new Error("truncated email envelope");
|
|
85
|
+
const metadataLength = new DataView(bytes.buffer).getUint32(0);
|
|
86
|
+
if (metadataLength > 131072 || total < 4 + metadataLength) throw new Error("invalid email envelope");
|
|
87
|
+
const metadataBytes = bytes.subarray(4, 4 + metadataLength);
|
|
88
|
+
assertFlatJson(metadataBytes, 2, 1032);
|
|
89
|
+
const metadata = JSON.parse(decoder.decode(metadataBytes));
|
|
90
|
+
if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) throw new Error("invalid email envelope");
|
|
91
|
+
const value = metadata;
|
|
92
|
+
const raw = bytes.subarray(4 + metadataLength, total);
|
|
93
|
+
if (raw.byteLength > 10485760 || typeof value.from !== "string" || value.from.length > 320 || typeof value.to !== "string" || value.to.length > 320 || typeof value.rawSize !== "number" || value.rawSize !== raw.byteLength || !value.headers || typeof value.headers !== "object" || Array.isArray(value.headers) || Object.keys(value.headers).length > 1024 || Object.values(value.headers).some((header) => typeof header !== "string") || value.viaDomain !== void 0 && (typeof value.viaDomain !== "string" || value.viaDomain.length > 253)) throw new Error("invalid email envelope");
|
|
94
|
+
return {
|
|
95
|
+
from: value.from,
|
|
96
|
+
to: value.to,
|
|
97
|
+
headers: value.headers,
|
|
98
|
+
raw,
|
|
99
|
+
rawSize: raw.byteLength,
|
|
100
|
+
...value.viaDomain ? { viaDomain: value.viaDomain } : {}
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function base64url(bytes) {
|
|
104
|
+
let binary = "";
|
|
105
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
106
|
+
return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
|
|
107
|
+
}
|
|
108
|
+
function unbase64url(value) {
|
|
109
|
+
if (!/^[A-Za-z0-9_-]+$/.test(value) || value.length > 1024) throw new Error("invalid receipt");
|
|
110
|
+
const binary = atob(value.replaceAll("-", "+").replaceAll("_", "/") + "=".repeat((4 - value.length % 4) % 4));
|
|
111
|
+
return Uint8Array.from(binary, (char) => char.charCodeAt(0));
|
|
112
|
+
}
|
|
113
|
+
async function receiptKey(secret, usage) {
|
|
114
|
+
if (!secret) throw new Error("Email signing key is not configured");
|
|
115
|
+
return crypto.subtle.importKey("raw", encoder.encode(secret), {
|
|
116
|
+
name: "HMAC",
|
|
117
|
+
hash: "SHA-256"
|
|
118
|
+
}, false, [usage]);
|
|
119
|
+
}
|
|
120
|
+
async function signEmailReceipt(claims, signingSecret) {
|
|
121
|
+
const payload = encoder.encode(JSON.stringify(claims));
|
|
122
|
+
const signature = new Uint8Array(await crypto.subtle.sign("HMAC", await receiptKey(signingSecret, "sign"), payload));
|
|
123
|
+
return `${base64url(payload)}.${base64url(signature)}`;
|
|
124
|
+
}
|
|
125
|
+
async function verifyEmailReceipt(token, signingSecret, expected, now = Date.now()) {
|
|
126
|
+
try {
|
|
127
|
+
if (token.length > 1536) return null;
|
|
128
|
+
const parts = token.split(".");
|
|
129
|
+
if (parts.length !== 2) return null;
|
|
130
|
+
const payload = unbase64url(parts[0]);
|
|
131
|
+
const signature = unbase64url(parts[1]);
|
|
132
|
+
if (payload.byteLength > 768 || signature.byteLength !== 32) return null;
|
|
133
|
+
if (!await crypto.subtle.verify("HMAC", await receiptKey(signingSecret, "verify"), Uint8Array.from(signature), Uint8Array.from(payload))) return null;
|
|
134
|
+
const claims = JSON.parse(decoder.decode(payload));
|
|
135
|
+
if (typeof claims !== "object" || claims === null) return null;
|
|
136
|
+
const value = claims;
|
|
137
|
+
if (value.version !== 1 || typeof value.receiptId !== "string" || !/^[a-f0-9-]{16,64}$/.test(value.receiptId) || value.projectId !== expected.projectId || value.deploymentId !== expected.deploymentId || value.recipient !== expected.recipient || typeof value.expiresAt !== "number" || !Number.isSafeInteger(value.expiresAt) || value.expiresAt < now || value.expiresAt > now + 6e4) return null;
|
|
138
|
+
return value;
|
|
139
|
+
} catch {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function frame(kind, payload) {
|
|
144
|
+
const bytes = new Uint8Array(5 + payload.byteLength);
|
|
145
|
+
new DataView(bytes.buffer).setUint32(0, payload.byteLength);
|
|
146
|
+
bytes[4] = kind;
|
|
147
|
+
bytes.set(payload, 5);
|
|
148
|
+
return bytes;
|
|
149
|
+
}
|
|
150
|
+
function encodeEmailActionMetadata(action) {
|
|
151
|
+
return encoder.encode(JSON.stringify(action.type === "reply" ? {
|
|
152
|
+
type: action.type,
|
|
153
|
+
from: action.from,
|
|
154
|
+
to: action.to
|
|
155
|
+
} : action));
|
|
156
|
+
}
|
|
157
|
+
function emailActionWireSizeFromMetadata(action, metadata) {
|
|
158
|
+
return 5 + metadata.byteLength + (action.type === "reply" ? 5 + action.raw.byteLength : 0);
|
|
159
|
+
}
|
|
160
|
+
function encodeEmailActions(actions) {
|
|
161
|
+
if (actions.length > 100) throw new Error("too many email actions");
|
|
162
|
+
let index = 0;
|
|
163
|
+
let pending = null;
|
|
164
|
+
let total = 0;
|
|
165
|
+
return new ReadableStream({ pull(controller) {
|
|
166
|
+
if (pending) {
|
|
167
|
+
controller.enqueue(pending);
|
|
168
|
+
pending = null;
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (index === actions.length) {
|
|
172
|
+
controller.enqueue(frame(0, /* @__PURE__ */ new Uint8Array()));
|
|
173
|
+
controller.close();
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const action = actions[index++];
|
|
177
|
+
const metadata = encodeEmailActionMetadata(action);
|
|
178
|
+
if (metadata.byteLength > 8192) throw new Error("email action metadata too large");
|
|
179
|
+
if (action.type === "reply") {
|
|
180
|
+
if (action.raw.byteLength > 5242880) throw new Error("email reply too large");
|
|
181
|
+
pending = frame(2, action.raw);
|
|
182
|
+
}
|
|
183
|
+
total += emailActionWireSizeFromMetadata(action, metadata);
|
|
184
|
+
if (total + 5 > 8388608) throw new Error("email actions too large");
|
|
185
|
+
controller.enqueue(frame(1, metadata));
|
|
186
|
+
} });
|
|
187
|
+
}
|
|
188
|
+
/** Read each frame with fixed ceilings before parsing metadata or allocating a reply. */
|
|
189
|
+
async function readEmailActions(stream, deadlineMs = 15e3) {
|
|
190
|
+
const reader = stream.getReader();
|
|
191
|
+
const actions = [];
|
|
192
|
+
let stash = /* @__PURE__ */ new Uint8Array(0);
|
|
193
|
+
let total = 0;
|
|
194
|
+
const deadline = Date.now() + deadlineMs;
|
|
195
|
+
async function readChunk() {
|
|
196
|
+
const remaining = deadline - Date.now();
|
|
197
|
+
if (remaining <= 0) throw new Error("email response timeout");
|
|
198
|
+
let timer;
|
|
199
|
+
return Promise.race([reader.read(), new Promise((_, reject) => {
|
|
200
|
+
timer = setTimeout(() => reject(/* @__PURE__ */ new Error("email response timeout")), remaining);
|
|
201
|
+
})]).finally(() => {
|
|
202
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
async function exact(length) {
|
|
206
|
+
const result = new Uint8Array(length);
|
|
207
|
+
let offset = 0;
|
|
208
|
+
while (offset < length) {
|
|
209
|
+
if (stash.byteLength === 0) {
|
|
210
|
+
const chunk = await readChunk();
|
|
211
|
+
if (chunk.done) throw new Error("truncated email response");
|
|
212
|
+
total += chunk.value.byteLength;
|
|
213
|
+
if (total > 8388608) throw new Error("email response too large");
|
|
214
|
+
stash = chunk.value;
|
|
215
|
+
}
|
|
216
|
+
const used = Math.min(stash.byteLength, length - offset);
|
|
217
|
+
result.set(stash.subarray(0, used), offset);
|
|
218
|
+
stash = stash.subarray(used);
|
|
219
|
+
offset += used;
|
|
220
|
+
}
|
|
221
|
+
return result;
|
|
222
|
+
}
|
|
223
|
+
try {
|
|
224
|
+
for (;;) {
|
|
225
|
+
const head = await exact(5);
|
|
226
|
+
const length = new DataView(head.buffer).getUint32(0);
|
|
227
|
+
const kind = head[4];
|
|
228
|
+
if (kind === 0) {
|
|
229
|
+
if (length !== 0 || stash.byteLength !== 0) throw new Error("invalid email response tail");
|
|
230
|
+
if (!(await readChunk()).done) throw new Error("invalid email response tail");
|
|
231
|
+
if (actions.some((action) => action.type === "reject") && actions.length !== 1) throw new Error("email reject cannot accompany delivery actions");
|
|
232
|
+
return actions;
|
|
233
|
+
}
|
|
234
|
+
if (kind !== 1 || length > 8192 || actions.length >= 100) throw new Error("invalid email action frame");
|
|
235
|
+
const metadataBytes = await exact(length);
|
|
236
|
+
assertFlatJson(metadataBytes, 2, 40);
|
|
237
|
+
const value = JSON.parse(decoder.decode(metadataBytes));
|
|
238
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("invalid email action");
|
|
239
|
+
const action = value;
|
|
240
|
+
if (action.type === "reply") {
|
|
241
|
+
if (typeof action.from !== "string" || typeof action.to !== "string" || Object.keys(action).length !== 3) throw new Error("invalid email reply");
|
|
242
|
+
const rawHead = await exact(5);
|
|
243
|
+
const rawLength = new DataView(rawHead.buffer).getUint32(0);
|
|
244
|
+
if (rawHead[4] !== 2 || rawLength > 5242880) throw new Error("invalid email reply frame");
|
|
245
|
+
actions.push({
|
|
246
|
+
type: "reply",
|
|
247
|
+
from: action.from,
|
|
248
|
+
to: action.to,
|
|
249
|
+
raw: await exact(rawLength)
|
|
250
|
+
});
|
|
251
|
+
} else if (action.type === "forward") {
|
|
252
|
+
if (typeof action.to !== "string" || Object.keys(action).length > 3 || action.headers !== void 0 && (typeof action.headers !== "object" || action.headers === null || Array.isArray(action.headers))) throw new Error("invalid email forward");
|
|
253
|
+
const headers = action.headers;
|
|
254
|
+
if (headers && (Object.keys(headers).length > 32 || Object.values(headers).some((v) => typeof v !== "string"))) throw new Error("invalid email headers");
|
|
255
|
+
actions.push({
|
|
256
|
+
type: "forward",
|
|
257
|
+
to: action.to,
|
|
258
|
+
...headers ? { headers } : {}
|
|
259
|
+
});
|
|
260
|
+
} else if (action.type === "reject") {
|
|
261
|
+
if (typeof action.reason !== "string" || Object.keys(action).length !== 2) throw new Error("invalid email reject");
|
|
262
|
+
actions.push({
|
|
263
|
+
type: "reject",
|
|
264
|
+
reason: action.reason
|
|
265
|
+
});
|
|
266
|
+
} else throw new Error("unknown email action");
|
|
267
|
+
}
|
|
268
|
+
} catch (error) {
|
|
269
|
+
reader.cancel().catch(() => {});
|
|
270
|
+
throw error;
|
|
271
|
+
} finally {
|
|
272
|
+
try {
|
|
273
|
+
reader.releaseLock();
|
|
274
|
+
} catch {}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
//#endregion
|
|
278
|
+
export { EMAIL_MAX_ACTIONS, EMAIL_MAX_INBOUND_BYTES, EMAIL_MAX_INBOUND_HEADERS, EMAIL_MAX_INBOUND_HEADER_BYTES, EMAIL_MAX_METADATA_BYTES, EMAIL_MAX_REPLY_BYTES, EMAIL_MAX_RESPONSE_BYTES, EMAIL_PROTOCOL_VERSION, EMAIL_RECEIPT_PATH, encodeEmailActions, encodeEmailEnvelope, readEmailActions, readEmailEnvelope, signEmailReceipt, verifyEmailReceipt };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,3 +1,115 @@
|
|
|
1
|
+
//#region src/authentication-recovery.d.ts
|
|
2
|
+
/** Owner recovery capabilities authorize a provider test, never a platform session. */
|
|
3
|
+
type PlatformAuthenticationRecovery = Readonly<{
|
|
4
|
+
installationId: string;
|
|
5
|
+
administratorUserId: string;
|
|
6
|
+
connectionId: string;
|
|
7
|
+
revision: number;
|
|
8
|
+
generation: number;
|
|
9
|
+
testId: string;
|
|
10
|
+
expiresAt: string;
|
|
11
|
+
signature: string;
|
|
12
|
+
}>;
|
|
13
|
+
export declare function platformAuthenticationRecoveryMessage(value: Omit<PlatformAuthenticationRecovery, "signature">): string;
|
|
14
|
+
//#endregion
|
|
15
|
+
//#region src/authentication.d.ts
|
|
16
|
+
/** Platform login and perimeter configuration are independent of app authentication. */
|
|
17
|
+
export declare const PLATFORM_LOGIN_KINDS: readonly ["github", "google", "oidc", "cloudflare-access"];
|
|
18
|
+
type PlatformLoginKind = (typeof PLATFORM_LOGIN_KINDS)[number];
|
|
19
|
+
type PlatformLoginConfiguration = Readonly<{
|
|
20
|
+
id: string;
|
|
21
|
+
kind: PlatformLoginKind;
|
|
22
|
+
label: string;
|
|
23
|
+
clientId: string;
|
|
24
|
+
/** Exact OIDC issuer; GitHub does not use OIDC. */
|
|
25
|
+
issuer?: string;
|
|
26
|
+
/** Google Workspace hosted domains, checked against the signed hd claim. */
|
|
27
|
+
allowedDomains: ReadonlyArray<string>;
|
|
28
|
+
/** An identity must belong to at least one configured group. */
|
|
29
|
+
requiredGroups: ReadonlyArray<string>;
|
|
30
|
+
}>;
|
|
31
|
+
type PlatformLoginDescriptor = Readonly<{
|
|
32
|
+
id: string;
|
|
33
|
+
kind: PlatformLoginKind;
|
|
34
|
+
label: string;
|
|
35
|
+
}>;
|
|
36
|
+
type PlatformAccessProtection = Readonly<{
|
|
37
|
+
teamDomain: string;
|
|
38
|
+
/** Each externally reachable platform origin has its own expected application audience. */
|
|
39
|
+
applications: ReadonlyArray<Readonly<{
|
|
40
|
+
origin: string;
|
|
41
|
+
audience: string;
|
|
42
|
+
}>>;
|
|
43
|
+
}>;
|
|
44
|
+
/** Operator-side coverage verification, bound to one administrator and settings revision. */
|
|
45
|
+
export declare function platformProtectionProofMessage(input: {
|
|
46
|
+
installationId: string;
|
|
47
|
+
origin: string;
|
|
48
|
+
userId: string;
|
|
49
|
+
sessionId: string;
|
|
50
|
+
revision: number;
|
|
51
|
+
lifecycleVersion: string;
|
|
52
|
+
protection: PlatformAccessProtection | null;
|
|
53
|
+
expiresAt: string;
|
|
54
|
+
}): string;
|
|
55
|
+
type PlatformAdmissionPolicy = Readonly<{
|
|
56
|
+
mode: "restricted" | "public";
|
|
57
|
+
}> | Readonly<{
|
|
58
|
+
mode: "company";
|
|
59
|
+
/** Explicitly trusted connections whose own configured restrictions must pass. */
|
|
60
|
+
connections: ReadonlyArray<string>;
|
|
61
|
+
/** A verified human Access identity qualifies, independently of the login method. */
|
|
62
|
+
access: boolean;
|
|
63
|
+
}>;
|
|
64
|
+
/** One persisted grant used by invited/allowlisted platform signup. */
|
|
65
|
+
type PlatformSignupAllowlistEntry = Readonly<{
|
|
66
|
+
type: "github" | "email";
|
|
67
|
+
pattern: string;
|
|
68
|
+
note?: string;
|
|
69
|
+
}> | Readonly<{
|
|
70
|
+
type: "identity";
|
|
71
|
+
/** Stable login connection namespace. */
|
|
72
|
+
connectionId: string;
|
|
73
|
+
/** Exact, case-sensitive provider subject. */
|
|
74
|
+
subject: string;
|
|
75
|
+
note?: string;
|
|
76
|
+
}>;
|
|
77
|
+
type PlatformSignupIdentity = Readonly<{
|
|
78
|
+
connectionId: string;
|
|
79
|
+
subject: string;
|
|
80
|
+
provider: string;
|
|
81
|
+
login: string;
|
|
82
|
+
/** Only pass a provider email after its verification claim has been validated. */
|
|
83
|
+
email: string | null;
|
|
84
|
+
}>;
|
|
85
|
+
export declare function parsePlatformSignupAllowlistEntry(value: unknown): PlatformSignupAllowlistEntry;
|
|
86
|
+
/** Match one verified login identity against persisted signup grants. */
|
|
87
|
+
export declare function isPlatformSignupAllowed(identity: PlatformSignupIdentity, entries: ReadonlyArray<PlatformSignupAllowlistEntry>): boolean;
|
|
88
|
+
type PlatformAuthenticationSetup = Readonly<{
|
|
89
|
+
connections: ReadonlyArray<Readonly<{
|
|
90
|
+
configuration: PlatformLoginConfiguration;
|
|
91
|
+
clientSecret: string;
|
|
92
|
+
}>>;
|
|
93
|
+
administratorConnectionId: string;
|
|
94
|
+
bootstrapCredentialHash: string;
|
|
95
|
+
admission: PlatformAdmissionPolicy;
|
|
96
|
+
protection: PlatformAccessProtection | null;
|
|
97
|
+
resetPending?: boolean;
|
|
98
|
+
}>;
|
|
99
|
+
export declare function parsePlatformAuthenticationSetup(value: unknown): PlatformAuthenticationSetup;
|
|
100
|
+
export declare const DEFAULT_PLATFORM_SESSION_SECONDS: number;
|
|
101
|
+
export declare const GOOGLE_OIDC_ISSUER = "https://accounts.google.com";
|
|
102
|
+
export declare const PLATFORM_AUTH_SETUP_PROOF_PREFIX = "void.platform.auth.setup.v1";
|
|
103
|
+
export declare function parsePlatformLoginId(value: unknown): string;
|
|
104
|
+
/** HTTPS syntax validation; outbound network policy must additionally validate resolved endpoints. */
|
|
105
|
+
export declare function parsePlatformIdentityUrl(value: unknown, name: string, originOnly?: boolean): string;
|
|
106
|
+
export declare function parsePlatformLoginDescriptor(value: unknown): PlatformLoginDescriptor;
|
|
107
|
+
export declare function parsePlatformLoginConfiguration(value: unknown): PlatformLoginConfiguration;
|
|
108
|
+
export declare function parsePlatformAccessProtection(value: unknown): PlatformAccessProtection | null;
|
|
109
|
+
export declare function parsePlatformAdmissionPolicy(value: unknown): PlatformAdmissionPolicy;
|
|
110
|
+
/** Run inside the settings mutation transaction, using the resulting enabled connections. */
|
|
111
|
+
export declare function validatePlatformAdmissionPolicy(policy: PlatformAdmissionPolicy, enabledConnections: ReadonlyArray<PlatformLoginConfiguration>, protection: PlatformAccessProtection | null): void;
|
|
112
|
+
//#endregion
|
|
1
113
|
//#region src/application.d.ts
|
|
2
114
|
/** Public application addressing, independent of the platform API's origin. */
|
|
3
115
|
type PlatformApplication = Readonly<{
|
|
@@ -61,6 +173,7 @@ type PlatformResourceNames = Readonly<{
|
|
|
61
173
|
dashboardWorker: string;
|
|
62
174
|
dispatchNamespace: string;
|
|
63
175
|
dispatchWorker: string;
|
|
176
|
+
emailGatewayWorker: string;
|
|
64
177
|
hyperdrive: string;
|
|
65
178
|
isrCacheKv: string;
|
|
66
179
|
legacyAssetsKv: string;
|
|
@@ -73,15 +186,21 @@ type PlatformResourceNames = Readonly<{
|
|
|
73
186
|
}>;
|
|
74
187
|
type PlatformDeploymentManifest = Readonly<{
|
|
75
188
|
application: PlatformApplication;
|
|
189
|
+
/** Zone that owns the installation's shared sender mail domain. */
|
|
190
|
+
emailSharedZoneId?: string;
|
|
191
|
+
/** Shared sender domain registered for managed email. */
|
|
192
|
+
emailSenderDomain?: string;
|
|
76
193
|
authentication: Readonly<{
|
|
77
|
-
providers: ReadonlyArray<
|
|
194
|
+
providers: ReadonlyArray<PlatformLoginKind>;
|
|
195
|
+
configurable?: true;
|
|
196
|
+
accessProtection?: true;
|
|
78
197
|
}>;
|
|
79
198
|
capabilities: PlatformCapabilities;
|
|
80
199
|
controlPlane: PlatformControlPlane;
|
|
81
200
|
displayName: string;
|
|
82
201
|
installationSlug: string;
|
|
83
202
|
resources: PlatformResourceNames;
|
|
84
|
-
schemaVersion: typeof PLATFORM_DEPLOYMENT_SCHEMA_VERSION | 2;
|
|
203
|
+
schemaVersion: typeof PLATFORM_DEPLOYMENT_SCHEMA_VERSION | 2 | 3;
|
|
85
204
|
}>;
|
|
86
205
|
type PlatformDeploymentManifestInput = Readonly<{
|
|
87
206
|
application: Readonly<{
|
|
@@ -91,8 +210,12 @@ type PlatformDeploymentManifestInput = Readonly<{
|
|
|
91
210
|
workersDev?: PlatformApplication["workersDev"];
|
|
92
211
|
zoneId?: string;
|
|
93
212
|
}>;
|
|
213
|
+
emailSharedZoneId?: string;
|
|
214
|
+
emailSenderDomain?: string;
|
|
94
215
|
authentication?: Readonly<{
|
|
95
|
-
providers?: ReadonlyArray<
|
|
216
|
+
providers?: ReadonlyArray<PlatformLoginKind>;
|
|
217
|
+
configurable?: true;
|
|
218
|
+
accessProtection?: true;
|
|
96
219
|
}>;
|
|
97
220
|
capabilities?: Partial<PlatformCapabilities>;
|
|
98
221
|
controlPlane?: Partial<PlatformControlPlane>;
|
|
@@ -119,7 +242,7 @@ export declare const PLATFORM_REMOTE_INSTALLATION_MANIFEST_KEY = "__void/platfor
|
|
|
119
242
|
export declare const PLATFORM_SANDBOX_DRAIN_KEY = "__void/platform/sandbox-drain/v1";
|
|
120
243
|
export declare const PLATFORM_R2_OWNERSHIP_MARKER_KEY = "__void/platform/ownership/v1";
|
|
121
244
|
export declare const PLATFORM_RESOURCE_OWNERSHIP_SCHEMA_VERSION: 1;
|
|
122
|
-
export declare const PLATFORM_USAGE_PROTOCOL_VERSION:
|
|
245
|
+
export declare const PLATFORM_USAGE_PROTOCOL_VERSION: 2;
|
|
123
246
|
type InstallationStatus = "draft" | "planned" | "provisioning" | "waiting-for-dns" | "waiting-for-oauth" | "deploying" | "verifying" | "installed" | "disabling" | "disabled" | "uninstalling" | "uninstalled" | "failed";
|
|
124
247
|
type ResourceOwnership = "created" | "adopted" | "external";
|
|
125
248
|
type InstallationResourceKind = "ai-gateway" | "analytics-dataset" | "dispatch-namespace" | "dns-record" | "hyperdrive" | "kv-namespace" | "queue" | "r2-bucket" | "worker" | "worker-custom-domain" | "worker-route" | "zone";
|
|
@@ -148,6 +271,14 @@ type PlatformRuntimeProvenance = Readonly<{
|
|
|
148
271
|
sourceRevision: string | null;
|
|
149
272
|
}>;
|
|
150
273
|
type PlatformInstallationManifest = Readonly<{
|
|
274
|
+
authenticationResources?: ReadonlyArray<Readonly<{
|
|
275
|
+
key: string;
|
|
276
|
+
kind: "application" | "service-token";
|
|
277
|
+
accountId: string;
|
|
278
|
+
id: string;
|
|
279
|
+
name: string;
|
|
280
|
+
ownership: "created" | "adopted";
|
|
281
|
+
}>>;
|
|
151
282
|
account: Readonly<{
|
|
152
283
|
id: string;
|
|
153
284
|
name: string;
|
|
@@ -173,7 +304,7 @@ type PlatformInstallationManifest = Readonly<{
|
|
|
173
304
|
steps: ReadonlyArray<InstallationStep>;
|
|
174
305
|
updatedAt: string;
|
|
175
306
|
/** Last usage protocol proven live across the installed Worker set. */
|
|
176
|
-
usageProtocolVersion: 0 | 1;
|
|
307
|
+
usageProtocolVersion: 0 | 1 | 2;
|
|
177
308
|
}>;
|
|
178
309
|
type PlatformResourceOwnershipMarker = Readonly<{
|
|
179
310
|
accountId: string;
|
|
@@ -208,7 +339,7 @@ export declare function createPlatformInstallationManifest(input: {
|
|
|
208
339
|
safetyProtocols?: Readonly<{
|
|
209
340
|
sandboxDrain: 0 | 1 | 2;
|
|
210
341
|
}>;
|
|
211
|
-
usageProtocolVersion?: 0 | 1;
|
|
342
|
+
usageProtocolVersion?: 0 | 1 | 2;
|
|
212
343
|
}): PlatformInstallationManifest;
|
|
213
344
|
export declare function setInstallationDesiredState(manifest: PlatformInstallationManifest, desiredState: PlatformInstallationManifest["desiredState"], updatedAt: string): PlatformInstallationManifest;
|
|
214
345
|
export declare function setInstallationEndpoints(manifest: PlatformInstallationManifest, endpoints: PlatformInstallationEndpoints, updatedAt: string): PlatformInstallationManifest;
|
|
@@ -219,21 +350,27 @@ export declare function parsePlatformInstallationManifest(value: unknown): Platf
|
|
|
219
350
|
//#endregion
|
|
220
351
|
//#region src/discovery.d.ts
|
|
221
352
|
export declare const PLATFORM_DISCOVERY_SCHEMA_VERSION: 1;
|
|
353
|
+
type PlatformAuthenticationDiscovery = Readonly<{
|
|
354
|
+
methods: ReadonlyArray<PlatformLoginDescriptor>;
|
|
355
|
+
protectedByAccess: boolean;
|
|
356
|
+
setupPending?: boolean;
|
|
357
|
+
}>;
|
|
222
358
|
type PlatformDiscoveryDocument = Readonly<{
|
|
223
359
|
apiUrl: string;
|
|
224
360
|
applicationDomain: string;
|
|
225
361
|
applicationRouting?: PlatformApplication;
|
|
226
|
-
authenticationProviders: ReadonlyArray<
|
|
362
|
+
authenticationProviders: ReadonlyArray<string>;
|
|
363
|
+
authentication?: PlatformAuthenticationDiscovery;
|
|
227
364
|
capabilities: PlatformCapabilities;
|
|
228
365
|
displayName: string;
|
|
229
366
|
installationId: string | null;
|
|
230
367
|
platformVersion: string;
|
|
231
368
|
proxyUrl: string;
|
|
232
|
-
schemaVersion: typeof PLATFORM_DISCOVERY_SCHEMA_VERSION | 2;
|
|
233
|
-
usageProtocolVersion: 0 | 1;
|
|
369
|
+
schemaVersion: typeof PLATFORM_DISCOVERY_SCHEMA_VERSION | 2 | 3;
|
|
370
|
+
usageProtocolVersion: 0 | 1 | 2;
|
|
234
371
|
}>;
|
|
235
372
|
export declare function createPlatformDiscoveryDocument(input: Omit<PlatformDiscoveryDocument, "schemaVersion" | "usageProtocolVersion"> & {
|
|
236
|
-
usageProtocolVersion?: 0 | 1;
|
|
373
|
+
usageProtocolVersion?: 0 | 1 | 2;
|
|
237
374
|
}): PlatformDiscoveryDocument;
|
|
238
375
|
export declare function parsePlatformDiscoveryDocument(value: unknown): PlatformDiscoveryDocument;
|
|
239
376
|
//#endregion
|
|
@@ -245,8 +382,9 @@ type PlatformProvisionedResourceIds = Readonly<{
|
|
|
245
382
|
routingKv: string;
|
|
246
383
|
zone: string;
|
|
247
384
|
}>;
|
|
248
|
-
type PlatformWorkerConfigSet = Readonly<Record<"api" | "dispatch" | "proxy" | "tail", Record<string, unknown>>>;
|
|
385
|
+
type PlatformWorkerConfigSet = Readonly<Record<"api" | "dispatch" | "proxy" | "tail" | "emailGateway", Record<string, unknown>>>;
|
|
249
386
|
export declare function createPlatformWorkerConfigs(input: {
|
|
387
|
+
authenticationDatabaseUncached?: boolean;
|
|
250
388
|
accountId: string;
|
|
251
389
|
apiUrl: string;
|
|
252
390
|
deployment: PlatformDeploymentManifest;
|
|
@@ -256,9 +394,10 @@ export declare function createPlatformWorkerConfigs(input: {
|
|
|
256
394
|
platformVersion: string;
|
|
257
395
|
publicEndpoints?: boolean;
|
|
258
396
|
proxyUrl: string;
|
|
397
|
+
emailGatewayUrl: string;
|
|
259
398
|
resources: PlatformProvisionedResourceIds;
|
|
260
|
-
runtimeEntrypoints: Readonly<Record<"api" | "dispatch" | "proxy" | "tail", string>>;
|
|
399
|
+
runtimeEntrypoints: Readonly<Record<"api" | "dispatch" | "proxy" | "tail" | "emailGateway", string>>;
|
|
261
400
|
trafficState?: "enabled" | "sandbox-drain" | "staging";
|
|
262
401
|
}): PlatformWorkerConfigSet;
|
|
263
402
|
//#endregion
|
|
264
|
-
export type { InstallationResource, InstallationResourceKind, InstallationStatus, InstallationStep, PlatformApplication, PlatformApplicationProject, PlatformApplicationRoute, PlatformCapabilities, PlatformControlPlane, PlatformDeploymentManifest, PlatformDeploymentManifestInput, PlatformDiscoveryDocument, PlatformInstallationEndpoints, PlatformInstallationManifest, PlatformProvisionedResourceIds, PlatformResourceNames, PlatformResourceOwnershipMarker, PlatformRuntimeProvenance, PlatformWorkerConfigSet, ResourceOwnership };
|
|
403
|
+
export type { InstallationResource, InstallationResourceKind, InstallationStatus, InstallationStep, PlatformAccessProtection, PlatformAdmissionPolicy, PlatformApplication, PlatformApplicationProject, PlatformApplicationRoute, PlatformAuthenticationDiscovery, PlatformAuthenticationRecovery, PlatformAuthenticationSetup, PlatformCapabilities, PlatformControlPlane, PlatformDeploymentManifest, PlatformDeploymentManifestInput, PlatformDiscoveryDocument, PlatformInstallationEndpoints, PlatformInstallationManifest, PlatformLoginConfiguration, PlatformLoginDescriptor, PlatformLoginKind, PlatformProvisionedResourceIds, PlatformResourceNames, PlatformResourceOwnershipMarker, PlatformRuntimeProvenance, PlatformSignupAllowlistEntry, PlatformSignupIdentity, PlatformWorkerConfigSet, ResourceOwnership };
|