lakebed 0.0.17 → 0.0.19
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 +51 -34
- package/package.json +1 -1
- package/src/anonymous-server.js +365 -8
- package/src/anonymous.js +244 -2
- package/src/auth.js +2 -3
- package/src/cli.js +319 -51
- package/src/client.d.ts +0 -1
- package/src/client.js +2 -2
- package/src/server.d.ts +49 -1
- package/src/server.js +53 -0
- package/src/source-runtime-worker.js +114 -1
- package/src/source-runtime.js +27 -0
- package/src/version.js +1 -1
package/src/anonymous.js
CHANGED
|
@@ -29,7 +29,8 @@ export const DEFAULT_ANONYMOUS_LIMITS = {
|
|
|
29
29
|
};
|
|
30
30
|
|
|
31
31
|
const expressionOps = new Set(["arg", "auth", "call", "row"]);
|
|
32
|
-
const authFields = new Set(["displayName", "email", "emailVerified", "isAuthenticated", "isGuest", "
|
|
32
|
+
const authFields = new Set(["displayName", "email", "emailVerified", "isAuthenticated", "isGuest", "picture", "provider", "userId"]);
|
|
33
|
+
const endpointMethodPattern = /^[A-Z0-9!#$%&'*+.^_`|~-]+$/;
|
|
33
34
|
const sourceModuleCache = new Map();
|
|
34
35
|
|
|
35
36
|
export class AnonymousCompilerError extends Error {
|
|
@@ -228,7 +229,6 @@ function createSymbolicAuth() {
|
|
|
228
229
|
emailVerified: new SymbolicValue(["auth", "emailVerified"], true),
|
|
229
230
|
isAuthenticated: new SymbolicValue(["auth", "isAuthenticated"], true),
|
|
230
231
|
isGuest: new SymbolicValue(["auth", "isGuest"], false),
|
|
231
|
-
name: new SymbolicValue(["auth", "name"], "Trace Guest"),
|
|
232
232
|
picture: new SymbolicValue(["auth", "picture"], "https://example.test/avatar.png"),
|
|
233
233
|
provider: new SymbolicValue(["auth", "provider"], "google"),
|
|
234
234
|
userId: new SymbolicValue(["auth", "userId"], "guest:trace")
|
|
@@ -248,6 +248,8 @@ function createSymbolicRow({ auth, idExpr, scanId, schema, tableName }) {
|
|
|
248
248
|
row[fieldName] = auth.userId;
|
|
249
249
|
} else if (fieldName === "authorName") {
|
|
250
250
|
row[fieldName] = auth.displayName;
|
|
251
|
+
} else if (fieldName === "authorPicture") {
|
|
252
|
+
row[fieldName] = auth.picture;
|
|
251
253
|
} else if (field.kind === "boolean") {
|
|
252
254
|
row[fieldName] = true;
|
|
253
255
|
} else {
|
|
@@ -486,6 +488,69 @@ function serializeSchema(schema) {
|
|
|
486
488
|
return { diagnostics, schema: cleanSchema };
|
|
487
489
|
}
|
|
488
490
|
|
|
491
|
+
function isReservedEndpointPath(path) {
|
|
492
|
+
return (
|
|
493
|
+
path === "/" ||
|
|
494
|
+
path === "/index.html" ||
|
|
495
|
+
path === "/client.js" ||
|
|
496
|
+
path === "/auth/callback" ||
|
|
497
|
+
path.startsWith("/auth/") ||
|
|
498
|
+
path === "/__lakebed" ||
|
|
499
|
+
path.startsWith("/__lakebed/") ||
|
|
500
|
+
path === "/__span" ||
|
|
501
|
+
path.startsWith("/__span/")
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function validateEndpointRoute({ method, path }, diagnosticPath, diagnostics) {
|
|
506
|
+
if (typeof method !== "string" || !endpointMethodPattern.test(method)) {
|
|
507
|
+
diagnostics.push(diagnostic(diagnosticPath, "Endpoint method must be a valid uppercase HTTP method."));
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
if (typeof path !== "string" || !path.startsWith("/") || path.startsWith("//") || path.includes("\\") || path.includes("?") || path.includes("#")) {
|
|
511
|
+
diagnostics.push(diagnostic(diagnosticPath, "Endpoint path must be an absolute app path like /webhooks/stripe."));
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
if (isReservedEndpointPath(path)) {
|
|
516
|
+
diagnostics.push(diagnostic(diagnosticPath, `Endpoint path ${path} is reserved by Lakebed.`));
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function serializeEndpoints(endpoints) {
|
|
521
|
+
const diagnostics = [];
|
|
522
|
+
const cleanEndpoints = {};
|
|
523
|
+
const seenRoutes = new Map();
|
|
524
|
+
|
|
525
|
+
for (const [name, endpoint] of Object.entries(endpoints ?? {})) {
|
|
526
|
+
const diagnosticPath = `server.index.endpoints.${name}`;
|
|
527
|
+
if (!endpoint || endpoint.kind !== "endpoint" || typeof endpoint.handler !== "function") {
|
|
528
|
+
diagnostics.push(diagnostic("server/index.ts", `Endpoint ${name} must be defined with endpoint({ method, path }, handler).`));
|
|
529
|
+
continue;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const method = String(endpoint.method ?? "").toUpperCase();
|
|
533
|
+
const path = String(endpoint.path ?? "");
|
|
534
|
+
validateEndpointRoute({ method, path }, diagnosticPath, diagnostics);
|
|
535
|
+
|
|
536
|
+
const routeKey = `${method} ${path}`;
|
|
537
|
+
const existing = seenRoutes.get(routeKey);
|
|
538
|
+
if (existing) {
|
|
539
|
+
diagnostics.push(diagnostic("server/index.ts", `Endpoint ${name} duplicates ${existing} at ${routeKey}.`));
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
542
|
+
seenRoutes.set(routeKey, name);
|
|
543
|
+
|
|
544
|
+
cleanEndpoints[name] = {
|
|
545
|
+
method,
|
|
546
|
+
op: "source",
|
|
547
|
+
path
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
return { diagnostics, endpoints: cleanEndpoints };
|
|
552
|
+
}
|
|
553
|
+
|
|
489
554
|
function compileQueryHandler({ handler, name, schema }) {
|
|
490
555
|
const { ctx, recorder } = createTraceContext({ mode: "query", schema });
|
|
491
556
|
try {
|
|
@@ -614,7 +679,12 @@ export async function createAnonymousArtifact({ app, clientOut, serverOut, sourc
|
|
|
614
679
|
const sourceFiles = await readSourceFiles(sourceStore);
|
|
615
680
|
const diagnostics = forbiddenSourceDiagnostics(sourceFiles, { allowAsync: Boolean(serverOut) });
|
|
616
681
|
const { diagnostics: schemaDiagnostics, schema } = serializeSchema(app.schema);
|
|
682
|
+
const { diagnostics: endpointDiagnostics, endpoints } = serializeEndpoints(app.endpoints);
|
|
617
683
|
diagnostics.push(...schemaDiagnostics);
|
|
684
|
+
diagnostics.push(...endpointDiagnostics);
|
|
685
|
+
if (!serverOut && Object.keys(endpoints).length > 0) {
|
|
686
|
+
diagnostics.push(diagnostic("server/index.ts", "Endpoints require the source runtime. Build with a bundled server module."));
|
|
687
|
+
}
|
|
618
688
|
|
|
619
689
|
if (diagnostics.length > 0) {
|
|
620
690
|
throw new AnonymousCompilerError(diagnostics);
|
|
@@ -630,6 +700,7 @@ export async function createAnonymousArtifact({ app, clientOut, serverOut, sourc
|
|
|
630
700
|
const sourceSnapshotHash = sha256(stableStringify(sourceManifest));
|
|
631
701
|
const server = serverBundle
|
|
632
702
|
? {
|
|
703
|
+
endpoints,
|
|
633
704
|
helpers: {},
|
|
634
705
|
imports: ["lakebed/server"],
|
|
635
706
|
mutations: Object.fromEntries(Object.keys(app.mutations ?? {}).map((name) => [name, { op: "source" }])),
|
|
@@ -673,6 +744,7 @@ export async function createAnonymousArtifact({ app, clientOut, serverOut, sourc
|
|
|
673
744
|
maxValueBytes: DEFAULT_ANONYMOUS_LIMITS.maxValueBytes
|
|
674
745
|
},
|
|
675
746
|
server: server ?? {
|
|
747
|
+
endpoints: {},
|
|
676
748
|
helpers: {},
|
|
677
749
|
imports: ["lakebed/server"],
|
|
678
750
|
mutations: compiled.mutations,
|
|
@@ -706,7 +778,9 @@ export async function createClaimedArtifact({ app, clientOut, serverOut, sourceS
|
|
|
706
778
|
entry.message !== "Async server handlers are not part of the anonymous IR yet. Use synchronous Lakebed database operations."
|
|
707
779
|
);
|
|
708
780
|
const { diagnostics: schemaDiagnostics, schema } = serializeSchema(app.schema);
|
|
781
|
+
const { diagnostics: endpointDiagnostics, endpoints } = serializeEndpoints(app.endpoints);
|
|
709
782
|
diagnostics.push(...schemaDiagnostics);
|
|
783
|
+
diagnostics.push(...endpointDiagnostics);
|
|
710
784
|
if (diagnostics.length > 0) {
|
|
711
785
|
throw new AnonymousCompilerError(diagnostics);
|
|
712
786
|
}
|
|
@@ -738,6 +812,7 @@ export async function createClaimedArtifact({ app, clientOut, serverOut, sourceS
|
|
|
738
812
|
maxValueBytes: DEFAULT_ANONYMOUS_LIMITS.maxValueBytes
|
|
739
813
|
},
|
|
740
814
|
server: {
|
|
815
|
+
endpoints,
|
|
741
816
|
helpers: {},
|
|
742
817
|
imports: ["lakebed/server"],
|
|
743
818
|
mutations: Object.fromEntries(Object.keys(app.mutations ?? {}).map((name) => [name, { op: "source" }])),
|
|
@@ -921,6 +996,30 @@ export function validateAnonymousArtifact(artifact, { allowClaimedSource = false
|
|
|
921
996
|
}
|
|
922
997
|
}
|
|
923
998
|
|
|
999
|
+
const seenEndpointRoutes = new Map();
|
|
1000
|
+
for (const [name, endpoint] of Object.entries(artifact.server?.endpoints ?? {})) {
|
|
1001
|
+
const path = `artifact.server.endpoints.${name}`;
|
|
1002
|
+
if (!isPlainObject(endpoint) || endpoint.op !== "source") {
|
|
1003
|
+
diagnostics.push(diagnostic(path, "Endpoint must be source-backed."));
|
|
1004
|
+
continue;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
validateEndpointRoute(endpoint, path, diagnostics);
|
|
1008
|
+
const routeKey = `${endpoint.method} ${endpoint.path}`;
|
|
1009
|
+
const existing = seenEndpointRoutes.get(routeKey);
|
|
1010
|
+
if (existing) {
|
|
1011
|
+
diagnostics.push(diagnostic(path, `Endpoint route duplicates ${existing}.`));
|
|
1012
|
+
}
|
|
1013
|
+
seenEndpointRoutes.set(routeKey, name);
|
|
1014
|
+
|
|
1015
|
+
if (artifact.server?.source === undefined) {
|
|
1016
|
+
diagnostics.push(diagnostic(path, "Source endpoint requires artifact.server.source."));
|
|
1017
|
+
}
|
|
1018
|
+
if (!sourceDeployTargets.has(artifact.deployTarget)) {
|
|
1019
|
+
diagnostics.push(diagnostic(path, "Source endpoint requires deployTarget anonymous-source or claimed-source."));
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
|
|
924
1023
|
if (artifact.server?.source !== undefined) {
|
|
925
1024
|
if (!sourceDeployTargets.has(artifact.deployTarget)) {
|
|
926
1025
|
diagnostics.push(diagnostic("artifact.server.source", "Server source bundles require deployTarget anonymous-source or claimed-source."));
|
|
@@ -1448,3 +1547,146 @@ export async function executeAnonymousMutation({ args = [], artifact, auth, depl
|
|
|
1448
1547
|
return null;
|
|
1449
1548
|
}, mutationTransactionOptions(limits));
|
|
1450
1549
|
}
|
|
1550
|
+
|
|
1551
|
+
function normalizeEndpointHeaders(headers = {}) {
|
|
1552
|
+
if (!headers) {
|
|
1553
|
+
return {};
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
if (typeof headers.entries === "function") {
|
|
1557
|
+
return Object.fromEntries(Array.from(headers.entries()).map(([key, value]) => [String(key), String(value)]));
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
if (Array.isArray(headers)) {
|
|
1561
|
+
return Object.fromEntries(headers.map(([key, value]) => [String(key), String(value)]));
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
if (isPlainObject(headers)) {
|
|
1565
|
+
return Object.fromEntries(Object.entries(headers).map(([key, value]) => [String(key), String(value)]));
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
return {};
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
function endpointBodyToBase64(body) {
|
|
1572
|
+
if (body === undefined || body === null) {
|
|
1573
|
+
return "";
|
|
1574
|
+
}
|
|
1575
|
+
if (typeof body === "string") {
|
|
1576
|
+
return Buffer.from(body, "utf8").toString("base64");
|
|
1577
|
+
}
|
|
1578
|
+
if (body instanceof ArrayBuffer) {
|
|
1579
|
+
return Buffer.from(body).toString("base64");
|
|
1580
|
+
}
|
|
1581
|
+
if (ArrayBuffer.isView(body)) {
|
|
1582
|
+
return Buffer.from(body.buffer, body.byteOffset, body.byteLength).toString("base64");
|
|
1583
|
+
}
|
|
1584
|
+
return Buffer.from(JSON.stringify(body ?? null), "utf8").toString("base64");
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
function endpointStatus(status, fallback = 200) {
|
|
1588
|
+
const parsed = Number(status);
|
|
1589
|
+
return Number.isInteger(parsed) && parsed >= 100 && parsed <= 599 ? parsed : fallback;
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
async function normalizeEndpointResponse(result) {
|
|
1593
|
+
if (result === undefined || result === null) {
|
|
1594
|
+
return { bodyBase64: "", headers: {}, status: 204 };
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
if (typeof Response !== "undefined" && result instanceof Response) {
|
|
1598
|
+
const body = Buffer.from(await result.arrayBuffer());
|
|
1599
|
+
return {
|
|
1600
|
+
bodyBase64: body.toString("base64"),
|
|
1601
|
+
headers: normalizeEndpointHeaders(result.headers),
|
|
1602
|
+
status: endpointStatus(result.status)
|
|
1603
|
+
};
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
if (isPlainObject(result) && result.kind === "response") {
|
|
1607
|
+
return {
|
|
1608
|
+
bodyBase64: endpointBodyToBase64(result.body),
|
|
1609
|
+
headers: normalizeEndpointHeaders(result.headers),
|
|
1610
|
+
status: endpointStatus(result.status)
|
|
1611
|
+
};
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
if (typeof result === "string") {
|
|
1615
|
+
return {
|
|
1616
|
+
bodyBase64: endpointBodyToBase64(result),
|
|
1617
|
+
headers: { "Content-Type": "text/plain; charset=utf-8" },
|
|
1618
|
+
status: 200
|
|
1619
|
+
};
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
return {
|
|
1623
|
+
bodyBase64: endpointBodyToBase64(result),
|
|
1624
|
+
headers: { "Content-Type": "application/json; charset=utf-8" },
|
|
1625
|
+
status: 200
|
|
1626
|
+
};
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
function createEndpointRequest(request) {
|
|
1630
|
+
const url = new URL(request.url ?? request.path ?? "/", "http://lakebed.local");
|
|
1631
|
+
const body = Buffer.from(request.bodyBase64 ?? "", "base64");
|
|
1632
|
+
const headers = new Map(
|
|
1633
|
+
Object.entries(normalizeEndpointHeaders(request.headers)).map(([key, value]) => [key.toLowerCase(), String(value)])
|
|
1634
|
+
);
|
|
1635
|
+
|
|
1636
|
+
return {
|
|
1637
|
+
headers: {
|
|
1638
|
+
entries() {
|
|
1639
|
+
return headers.entries();
|
|
1640
|
+
},
|
|
1641
|
+
get(name) {
|
|
1642
|
+
return headers.get(String(name).toLowerCase()) ?? null;
|
|
1643
|
+
},
|
|
1644
|
+
has(name) {
|
|
1645
|
+
return headers.has(String(name).toLowerCase());
|
|
1646
|
+
}
|
|
1647
|
+
},
|
|
1648
|
+
method: String(request.method ?? "GET").toUpperCase(),
|
|
1649
|
+
path: request.path ?? url.pathname,
|
|
1650
|
+
query: new URLSearchParams(url.searchParams),
|
|
1651
|
+
url: url.href,
|
|
1652
|
+
async bytes() {
|
|
1653
|
+
return new Uint8Array(body);
|
|
1654
|
+
},
|
|
1655
|
+
async json() {
|
|
1656
|
+
return JSON.parse(body.toString("utf8"));
|
|
1657
|
+
},
|
|
1658
|
+
async text() {
|
|
1659
|
+
return body.toString("utf8");
|
|
1660
|
+
}
|
|
1661
|
+
};
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
export async function executeAnonymousEndpoint({ artifact, auth, deployId, limits = DEFAULT_ANONYMOUS_LIMITS, name, request, sourceRuntime, state }) {
|
|
1665
|
+
const endpoint = artifact.server?.endpoints?.[name];
|
|
1666
|
+
if (!endpoint) {
|
|
1667
|
+
throw new Error(`Unknown endpoint: ${name}`);
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
if (!artifact.server?.source) {
|
|
1671
|
+
throw new Error(`Endpoint ${name} requires the source runtime.`);
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
if (sourceRuntime) {
|
|
1675
|
+
return sourceRuntime.executeEndpoint({ artifact, auth, deployId, limits, name, request, state });
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
const sourceApp = await loadSourceApp(artifact);
|
|
1679
|
+
const definition = sourceApp.endpoints?.[name];
|
|
1680
|
+
const handler = definition?.handler ?? definition;
|
|
1681
|
+
if (typeof handler !== "function") {
|
|
1682
|
+
throw new Error(`Unknown endpoint: ${name}`);
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
return state.transaction(deployId, async (tx) => {
|
|
1686
|
+
const source = await createSourceContext({ artifact, auth, deployId, state: tx });
|
|
1687
|
+
const result = await handler(source.ctx, createEndpointRequest(request));
|
|
1688
|
+
const response = await normalizeEndpointResponse(result);
|
|
1689
|
+
await source.flush(tx);
|
|
1690
|
+
return response;
|
|
1691
|
+
}, mutationTransactionOptions(limits));
|
|
1692
|
+
}
|
package/src/auth.js
CHANGED
|
@@ -81,15 +81,14 @@ function authFromClaims(claims) {
|
|
|
81
81
|
return null;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
const name = stringClaim(claims, "name");
|
|
84
|
+
const name = stringClaim(claims, "name")?.trim();
|
|
85
85
|
const email = stringClaim(claims, "email");
|
|
86
86
|
return {
|
|
87
|
-
displayName: name
|
|
87
|
+
displayName: name || "Google User",
|
|
88
88
|
email,
|
|
89
89
|
emailVerified: booleanClaim(claims, "email_verified"),
|
|
90
90
|
isAuthenticated: true,
|
|
91
91
|
isGuest: false,
|
|
92
|
-
name,
|
|
93
92
|
picture: stringClaim(claims, "picture"),
|
|
94
93
|
provider: "google",
|
|
95
94
|
userId: `google:${pairwiseSub}`
|