@volter/world-core 2.0.37 → 3.0.1
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 +4 -5
- package/app-route.cjs +12 -6
- package/app-route.d.cts +1 -1
- package/dist/app-route.cjs +12 -6
- package/dist/app-route.d.cts +1 -1
- package/dist/generated/pack-facts.json +1410 -3069
- package/dist/inject.cjs +64 -9
- package/dist/pack-facts.cjs +44 -0
- package/dist/src/actions.d.ts +3 -3
- package/dist/src/actions.js +22 -16
- package/dist/src/ancestry.d.ts +14 -2
- package/dist/src/ancestry.js +92 -2
- package/dist/src/anthropic-wire.d.ts +39 -0
- package/dist/src/anthropic-wire.js +136 -0
- package/dist/src/bytes.d.ts +7 -0
- package/dist/src/bytes.js +35 -0
- package/dist/src/changeset.d.ts +1 -1
- package/dist/src/changeset.js +0 -0
- package/dist/src/clickhouse/index.d.ts +3 -0
- package/dist/src/clickhouse/index.js +6 -0
- package/dist/src/clickhouse/sql.d.ts +233 -0
- package/dist/src/clickhouse/sql.js +4329 -0
- package/dist/src/clickhouse/types.d.ts +18 -0
- package/dist/src/clickhouse/types.js +47 -0
- package/dist/src/clickhouse/values.d.ts +146 -0
- package/dist/src/clickhouse/values.js +858 -0
- package/dist/src/client-bundle.js +2 -3
- package/dist/src/cors.d.ts +15 -0
- package/dist/src/cors.js +31 -0
- package/dist/src/derived-core.d.ts +487 -24
- package/dist/src/derived-core.js +788 -144
- package/dist/src/derived-real.d.ts +13 -0
- package/dist/src/derived-real.js +518 -0
- package/dist/src/derived.d.ts +35 -1
- package/dist/src/derived.js +61 -9
- package/dist/src/emit.js +1 -2
- package/dist/src/events.d.ts +206 -0
- package/dist/src/events.js +341 -0
- package/dist/src/executor.d.ts +3 -0
- package/dist/src/executor.js +19 -2
- package/dist/src/file-response.d.ts +6 -0
- package/dist/src/file-response.js +30 -0
- package/dist/src/fork.js +3 -2
- package/dist/src/git/history.d.ts +7 -0
- package/dist/src/git/history.js +24 -0
- package/dist/src/git/index.d.ts +1 -0
- package/dist/src/git/index.js +1 -0
- package/dist/src/git/lfs.d.ts +28 -0
- package/dist/src/git/lfs.js +66 -0
- package/dist/src/git/objects.js +3 -8
- package/dist/src/git/smart-http.d.ts +3 -1
- package/dist/src/git/smart-http.js +67 -6
- package/dist/src/graphql-wire.d.ts +29 -0
- package/dist/src/graphql-wire.js +101 -0
- package/dist/src/grpc-wire.d.ts +67 -0
- package/dist/src/grpc-wire.js +170 -0
- package/dist/src/h2.d.ts +40 -0
- package/dist/src/h2.js +656 -0
- package/dist/src/head.d.ts +32 -3
- package/dist/src/head.js +161 -40
- package/dist/src/history.d.ts +1 -1
- package/dist/src/history.js +6 -6
- package/dist/src/hpack.json +1 -0
- package/dist/src/index.d.ts +64 -75
- package/dist/src/index.js +58 -101
- package/dist/src/log.js +28 -19
- package/dist/src/machines.d.ts +50 -0
- package/dist/src/machines.js +151 -0
- package/dist/src/managed-database.d.ts +86 -0
- package/dist/src/managed-database.js +283 -0
- package/dist/src/multipart.d.ts +11 -0
- package/dist/src/multipart.js +51 -0
- package/dist/src/observe.d.ts +15 -5
- package/dist/src/observe.js +23 -9
- package/dist/src/openai-wire.d.ts +108 -0
- package/dist/src/openai-wire.js +337 -0
- package/dist/src/pack-assets.d.ts +3 -4
- package/dist/src/pack-assets.js +15 -10
- package/dist/src/pack-fetch.d.ts +77 -0
- package/dist/src/pack-fetch.js +449 -0
- package/dist/src/pack-paths.d.ts +12 -0
- package/dist/src/pack-paths.js +86 -0
- package/dist/src/packRegistry.d.ts +69 -162
- package/dist/src/packRegistry.js +55 -20
- package/dist/src/people.d.ts +13 -0
- package/dist/src/people.js +18 -0
- package/dist/src/placeholder-image.d.ts +5 -0
- package/dist/src/placeholder-image.js +114 -0
- package/dist/src/protobuf.d.ts +28 -0
- package/dist/src/protobuf.js +332 -0
- package/dist/src/redis/engine.js +1 -1
- package/dist/src/request-scope.d.ts +1 -1
- package/dist/src/request-scope.js +6 -4
- package/dist/src/resource-blob.d.ts +5 -0
- package/dist/src/resource-blob.js +11 -0
- package/dist/src/runtime.d.ts +85 -0
- package/dist/src/runtime.js +104 -0
- package/dist/src/s3/wire.d.ts +60 -0
- package/dist/src/s3/wire.js +157 -0
- package/dist/src/scenario.d.ts +3 -0
- package/dist/src/scenario.js +2 -0
- package/dist/src/schema-sample.d.ts +1 -0
- package/dist/src/schema-sample.js +21 -0
- package/dist/src/sealed-box.d.ts +14 -0
- package/dist/src/sealed-box.js +225 -0
- package/dist/src/serve-http.d.ts +14 -0
- package/dist/src/serve-http.js +27 -3
- package/dist/src/serve.d.ts +6 -0
- package/dist/src/serve.js +69 -14
- package/dist/src/signing.d.ts +135 -0
- package/dist/src/signing.js +222 -0
- package/dist/src/sigv4.d.ts +48 -0
- package/dist/src/sigv4.js +167 -0
- package/dist/src/smtp.d.ts +16 -0
- package/dist/src/smtp.js +72 -0
- package/dist/src/sockets.d.ts +51 -0
- package/dist/src/sockets.js +90 -0
- package/dist/src/state-system.d.ts +1 -0
- package/dist/src/state-system.js +1 -1
- package/dist/src/storage.d.ts +1 -1
- package/dist/src/storage.js +3 -3
- package/dist/src/trace-context.js +1 -1
- package/dist/src/twin-fetch.d.ts +0 -7
- package/dist/src/twin-fetch.js +0 -14
- package/dist/src/vendor-call.d.ts +6 -0
- package/dist/src/vendor-call.js +41 -0
- package/dist/src/world-store.js +1 -1
- package/dist/vendor-hosts.cjs +36 -125
- package/dist/vendor-hosts.d.cts +8 -0
- package/generated/pack-facts.json +1410 -3069
- package/inject.cjs +64 -9
- package/pack-facts.cjs +44 -0
- package/package.json +17 -3
- package/src/actions.ts +23 -16
- package/src/ancestry.ts +74 -2
- package/src/anthropic-wire.ts +137 -0
- package/src/bytes.ts +42 -0
- package/src/changeset.ts +5 -5
- package/src/clickhouse/index.ts +6 -0
- package/src/clickhouse/sql.ts +3059 -0
- package/src/clickhouse/types.ts +44 -0
- package/src/clickhouse/values.ts +697 -0
- package/src/client-bundle.ts +2 -3
- package/src/cors.ts +34 -0
- package/src/derived-core.ts +1013 -146
- package/src/derived-real.ts +434 -0
- package/src/derived.ts +73 -3
- package/src/emit.ts +1 -2
- package/src/events.ts +449 -0
- package/src/executor.ts +24 -2
- package/src/file-response.ts +27 -0
- package/src/fork.ts +3 -2
- package/src/git/history.ts +19 -0
- package/src/git/index.ts +1 -0
- package/src/git/lfs.ts +67 -0
- package/src/git/objects.ts +3 -5
- package/src/git/smart-http.ts +56 -6
- package/src/graphql-wire.ts +106 -0
- package/src/grpc-wire.ts +159 -0
- package/src/h2.ts +627 -0
- package/src/head.ts +132 -41
- package/src/history.ts +6 -6
- package/src/hpack.json +1 -0
- package/src/index.ts +82 -329
- package/src/log.ts +27 -18
- package/src/machines.ts +151 -0
- package/src/managed-database.ts +299 -0
- package/src/multipart.ts +51 -0
- package/src/observe.ts +31 -15
- package/src/openai-wire.ts +371 -0
- package/src/pack-assets.ts +15 -11
- package/src/pack-fetch.ts +458 -0
- package/src/pack-paths.ts +72 -0
- package/src/packRegistry.ts +79 -167
- package/src/people.ts +31 -0
- package/src/placeholder-image.ts +88 -0
- package/src/protobuf.ts +251 -0
- package/src/redis/engine.ts +1 -1
- package/src/request-scope.ts +8 -4
- package/src/resource-blob.ts +13 -0
- package/src/runtime.ts +344 -0
- package/src/s3/wire.ts +172 -0
- package/src/scenario.ts +4 -0
- package/src/schema-sample.ts +24 -0
- package/src/sealed-box.ts +182 -0
- package/src/serve-http.ts +31 -3
- package/src/serve.ts +58 -14
- package/src/signing.ts +231 -0
- package/src/sigv4.ts +158 -0
- package/src/smtp.ts +76 -0
- package/src/sockets.ts +140 -0
- package/src/state-system.ts +2 -2
- package/src/storage.ts +3 -3
- package/src/trace-context.ts +1 -1
- package/src/twin-fetch.ts +0 -20
- package/src/vendor-call.ts +41 -0
- package/src/world-store.ts +1 -1
- package/vendor-hosts.cjs +36 -125
- package/vendor-hosts.d.cts +8 -0
- package/dist/src/mirror-shell.d.ts +0 -2
- package/dist/src/mirror-shell.js +0 -13
- package/dist/src/v1-removed.d.ts +0 -159
- package/dist/src/v1-removed.js +0 -124
- package/src/mirror-shell.ts +0 -15
- package/src/v1-removed.ts +0 -172
package/dist/src/executor.d.ts
CHANGED
|
@@ -114,7 +114,10 @@ export type CredentialCustody = {
|
|
|
114
114
|
open: () => Promise<CredentialPayload>;
|
|
115
115
|
seal: (credential: CredentialPayload) => Promise<void> | void;
|
|
116
116
|
};
|
|
117
|
+
/** `vendor`: the vendor whose rate budget every call is charged to (D8): the call is priced and reserved before it is
|
|
118
|
+
* made, refused when the budget is spent, and settled with the vendor's answer (its back-off honoured). */
|
|
117
119
|
export type RemoteExecuteOptions = {
|
|
118
120
|
custody?: CredentialCustody;
|
|
121
|
+
vendor?: string;
|
|
119
122
|
};
|
|
120
123
|
export declare function buildRemoteExecute(origin: URL, credential: CredentialPayload, auth?: TwinAuthStrategy, vendorHosts?: readonly HostRule[], options?: RemoteExecuteOptions): RemoteExecute;
|
package/dist/src/executor.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { RateBudget } from "./rateBudget.js";
|
|
1
2
|
const MAX_ORIGIN_LENGTH = 2048;
|
|
2
3
|
function isLoopbackHost(hostname) {
|
|
3
4
|
return hostname === '127.0.0.1' || hostname === 'localhost' || hostname === '[::1]' || hostname === '::1';
|
|
@@ -129,8 +130,24 @@ async function awsSigV4Headers(input) {
|
|
|
129
130
|
};
|
|
130
131
|
}
|
|
131
132
|
export function buildRemoteExecute(origin, credential, auth, vendorHosts, options = {}) {
|
|
132
|
-
|
|
133
|
-
|
|
133
|
+
const execute = auth?.in === 'exchange' ? exchangeExecute(origin, credential, auth, vendorHosts, options.custody) : direct(origin, credential, auth, vendorHosts);
|
|
134
|
+
return options.vendor === undefined ? execute : budgeted(options.vendor, execute, options.custody?.key ?? JSON.stringify(credential.headers));
|
|
135
|
+
}
|
|
136
|
+
/** Every live call charged to the vendor's rate budget (rateBudget.ts, D8): a call the budget refuses is never made;
|
|
137
|
+
* the ledger is the credential's (only its hash names it). */
|
|
138
|
+
function budgeted(vendor, execute, token) {
|
|
139
|
+
const budget = new RateBudget({ vendor, token });
|
|
140
|
+
return async (request) => {
|
|
141
|
+
const url = new URL(request.path, 'http://budget.invalid');
|
|
142
|
+
const weight = budget.weightFor(`${request.method.toUpperCase()} ${url.pathname}`, Object.fromEntries(url.searchParams));
|
|
143
|
+
const reservation = budget.checkBudget(weight);
|
|
144
|
+
const answer = await execute(request);
|
|
145
|
+
budget.recordCall(weight, answer.headers, { status: answer.status, reservation });
|
|
146
|
+
return answer;
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
/** The call made as the root's credential is applied by the strategy (header replacement when none). */
|
|
150
|
+
function direct(origin, credential, auth, vendorHosts) {
|
|
134
151
|
return async ({ method, path, headers, body, responseType, presigned }) => {
|
|
135
152
|
// A PRESIGNED UPLOAD URL the vendor returned: its own signature is its authorization, so the
|
|
136
153
|
// sealed credential never goes with it, whatever host it names. A host other than the root's
|
|
@@ -1,3 +1,9 @@
|
|
|
1
1
|
/** The content type a file's extension implies; `application/octet-stream` for the rest. */
|
|
2
2
|
export declare function contentTypeOf(path: string): string;
|
|
3
3
|
export declare function fileResponse(path: string, init?: ResponseInit): Response;
|
|
4
|
+
/**
|
|
5
|
+
* Stored bytes as a response, honouring a single `Range: bytes=` as RFC 9110 §14 has it: a range that does not parse, a
|
|
6
|
+
* first-byte-pos past its last-byte-pos (`bytes=5-2`) included, is ignored and the whole body is a 200; a well-formed
|
|
7
|
+
* range that starts past the end is a 416. `nosniff` stops a browser second-guessing the content type the vendor gave.
|
|
8
|
+
*/
|
|
9
|
+
export declare function bytesResponse(request: Request, bytes: Uint8Array, contentType: string, cacheControl?: string): Response;
|
|
@@ -20,3 +20,33 @@ export function fileResponse(path, init) {
|
|
|
20
20
|
headers.set('content-type', contentTypeOf(path));
|
|
21
21
|
return new Response(readFileSync(path), { ...init, headers });
|
|
22
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* Stored bytes as a response, honouring a single `Range: bytes=` as RFC 9110 §14 has it: a range that does not parse, a
|
|
25
|
+
* first-byte-pos past its last-byte-pos (`bytes=5-2`) included, is ignored and the whole body is a 200; a well-formed
|
|
26
|
+
* range that starts past the end is a 416. `nosniff` stops a browser second-guessing the content type the vendor gave.
|
|
27
|
+
*/
|
|
28
|
+
export function bytesResponse(request, bytes, contentType, cacheControl = 'public, max-age=604800, immutable') {
|
|
29
|
+
const total = bytes.length;
|
|
30
|
+
const base = { 'content-type': contentType, 'x-content-type-options': 'nosniff', 'accept-ranges': 'bytes', 'cache-control': cacheControl };
|
|
31
|
+
const range = /^bytes=(\d*)-(\d*)$/.exec((request.headers.get('range') ?? '').trim());
|
|
32
|
+
let start = 0;
|
|
33
|
+
let end = total - 1;
|
|
34
|
+
const valid = range !== null && (range[1] !== '' || range[2] !== '') && !(range[1] !== '' && range[2] !== '' && Number(range[1]) > Number(range[2]));
|
|
35
|
+
if (valid) {
|
|
36
|
+
if (range[1] === '') {
|
|
37
|
+
start = Math.max(0, total - Number(range[2]));
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
start = Number(range[1]);
|
|
41
|
+
end = range[2] === '' ? total - 1 : Math.min(Number(range[2]), total - 1);
|
|
42
|
+
}
|
|
43
|
+
if (start >= total || (range[1] === '' && Number(range[2]) === 0))
|
|
44
|
+
return new Response(null, { status: 416, headers: { ...base, 'content-range': `bytes */${total}` } });
|
|
45
|
+
}
|
|
46
|
+
const partial = start !== 0 || end !== total - 1;
|
|
47
|
+
const body = bytes.slice(start, end + 1);
|
|
48
|
+
return new Response(request.method === 'HEAD' ? null : new Blob([body]), {
|
|
49
|
+
status: partial ? 206 : 200,
|
|
50
|
+
headers: { ...base, 'content-length': String(body.length), ...(partial ? { 'content-range': `bytes ${start}-${end}/${total}` } : {}) },
|
|
51
|
+
});
|
|
52
|
+
}
|
package/dist/src/fork.js
CHANGED
|
@@ -3,7 +3,7 @@ import { captureHistory, historyEntries, historyLength, historyPrefix, readHisto
|
|
|
3
3
|
// The kernel registers parent ownership before publishing the pointer. Lifecycle removal
|
|
4
4
|
// refuses while a child depends on that storage; direct filesystem deletion bypasses it.
|
|
5
5
|
import { dirname, join } from 'node:path';
|
|
6
|
-
import { checkParent,
|
|
6
|
+
import { checkParent, withHistoryLock } from "./ancestry.js";
|
|
7
7
|
import { getActiveWorldStore } from "./world-store.js";
|
|
8
8
|
import { writeBranchMeta } from "./log.js";
|
|
9
9
|
import { twinResources } from "./serve.js";
|
|
@@ -17,7 +17,8 @@ function forkMetaPath(service, root) {
|
|
|
17
17
|
* References the base log at a position, then records the divergence baseline. `occurredAt` is supplied by the caller (deterministic; no clock).
|
|
18
18
|
*/
|
|
19
19
|
export function forkTwin(opts) {
|
|
20
|
-
|
|
20
|
+
// under the new branch's history lock; capturing the base nests the base's inside it (child before ancestor)
|
|
21
|
+
return withHistoryLock(worldPaths(opts.service, opts.toRoot).dir, () => {
|
|
21
22
|
// A BRANCH IS A POINTER (log.ts): the base root and the position in its logs, nothing copied.
|
|
22
23
|
// The branch reads the base's parent view and branch log up to that position as its own
|
|
23
24
|
// parent, and records only its own entries from there. The base keeps appending past the
|
|
@@ -34,3 +34,10 @@ export declare function diffFiles(store: GitObjectStore, baseSha: string, headSh
|
|
|
34
34
|
/** Three-way line merge. Return null for overlapping edits; preserve exact line endings.
|
|
35
35
|
* Binary payloads belong to the caller's conflict path, never a text decoder's replacement. */
|
|
36
36
|
export declare function mergeText(base: string, ours: string, theirs: string): string | null;
|
|
37
|
+
/** The blob at a path in a commit (walking its trees segment by segment), or null when the commit, a directory on the
|
|
38
|
+
* way or the file is not there. A host that serves a file at a revision (the Hugging Face Hub's `resolve`) reads it. */
|
|
39
|
+
export declare function blobAt(store: GitObjectStore, commit: string, path: string): Promise<{
|
|
40
|
+
sha: string;
|
|
41
|
+
mode: string;
|
|
42
|
+
bytes: Uint8Array;
|
|
43
|
+
} | null>;
|
package/dist/src/git/history.js
CHANGED
|
@@ -296,3 +296,27 @@ export function mergeText(base, ours, theirs) {
|
|
|
296
296
|
result.splice(h.start, h.end - h.start, ...h.lines);
|
|
297
297
|
return result.map(line => JSON.parse(line)).join('');
|
|
298
298
|
}
|
|
299
|
+
/** The blob at a path in a commit (walking its trees segment by segment), or null when the commit, a directory on the
|
|
300
|
+
* way or the file is not there. A host that serves a file at a revision (the Hugging Face Hub's `resolve`) reads it. */
|
|
301
|
+
export async function blobAt(store, commit, path) {
|
|
302
|
+
const c = await store.read(commit);
|
|
303
|
+
if (!c || c.type !== 'commit')
|
|
304
|
+
return null;
|
|
305
|
+
let tree = decodeCommit(c.payload).tree;
|
|
306
|
+
const parts = path.split('/').filter(Boolean);
|
|
307
|
+
for (const [i, name] of parts.entries()) {
|
|
308
|
+
const t = await store.read(tree);
|
|
309
|
+
if (!t || t.type !== 'tree')
|
|
310
|
+
return null;
|
|
311
|
+
const e = decodeTree(t.payload).find((x) => x.name === name);
|
|
312
|
+
if (!e)
|
|
313
|
+
return null;
|
|
314
|
+
if (i < parts.length - 1) {
|
|
315
|
+
tree = e.sha;
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
const b = await store.read(e.sha);
|
|
319
|
+
return b && b.type === 'blob' ? { sha: e.sha, mode: e.mode, bytes: b.payload } : null;
|
|
320
|
+
}
|
|
321
|
+
return null;
|
|
322
|
+
}
|
package/dist/src/git/index.d.ts
CHANGED
package/dist/src/git/index.js
CHANGED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export declare const LFS_TYPE = "application/vnd.git-lfs+json";
|
|
2
|
+
export type LfsObject = {
|
|
3
|
+
oid: string;
|
|
4
|
+
size: number;
|
|
5
|
+
};
|
|
6
|
+
/** Where a pack keeps its objects: by oid. */
|
|
7
|
+
export type LfsStore = {
|
|
8
|
+
get(oid: string): Promise<Uint8Array | null>;
|
|
9
|
+
put(oid: string, bytes: Uint8Array): Promise<void>;
|
|
10
|
+
};
|
|
11
|
+
/** A pointer file's object (`version https://git-lfs.github.com/spec/v1`, `oid sha256:<hex>`, `size <n>`), or null
|
|
12
|
+
* for any other file. */
|
|
13
|
+
export declare function parseLfsPointer(file: Uint8Array | string): LfsObject | null;
|
|
14
|
+
/** A pointer file's text for an object. */
|
|
15
|
+
export declare const lfsPointer: (o: LfsObject) => string;
|
|
16
|
+
/** An object's sha256, as its oid. */
|
|
17
|
+
export declare function lfsOid(bytes: Uint8Array): Promise<string>;
|
|
18
|
+
/** `POST …/info/lfs/objects/batch`: for each object asked, an upload's action (none when the object is held already) or
|
|
19
|
+
* a download's (an error when it is not held), by the basic transfer. `href` names where an object goes up or comes
|
|
20
|
+
* down; `header` is sent with each action. */
|
|
21
|
+
export declare function lfsBatch(body: unknown, opts: {
|
|
22
|
+
store: LfsStore;
|
|
23
|
+
href: (operation: 'upload' | 'download', o: LfsObject) => string;
|
|
24
|
+
header?: Record<string, string>;
|
|
25
|
+
}): Promise<Response>;
|
|
26
|
+
/** A basic transfer's upload (`PUT` of the object's bytes): kept when they hash to the oid and have the declared size
|
|
27
|
+
* (the size is the request's Content-Length when the caller has no other), else refused and nothing kept. */
|
|
28
|
+
export declare function lfsUpload(bytes: Uint8Array, o: LfsObject, store: LfsStore): Promise<Response>;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// GIT LFS (architecture, "The tree contract": Git LFS is the git library's too): the batch API git-lfs speaks
|
|
2
|
+
// (https://github.com/git-lfs/git-lfs/blob/main/docs/api/batch.md, the basic transfer), a pointer file read
|
|
3
|
+
// (https://github.com/git-lfs/git-lfs/blob/main/docs/spec.md), and an upload accepted only when its bytes hash to the
|
|
4
|
+
// object's oid and its size is the one declared. The objects are the caller's (a pack's resource blobs, by oid); the
|
|
5
|
+
// caller says where objects go up and come down (the vendor's own hosts) and decides who may do either before it calls.
|
|
6
|
+
// Importing it does nothing.
|
|
7
|
+
export const LFS_TYPE = 'application/vnd.git-lfs+json';
|
|
8
|
+
const OID = /^[0-9a-f]{64}$/;
|
|
9
|
+
const lfsJson = (status, body) => Response.json(body, { status, headers: { 'content-type': LFS_TYPE } });
|
|
10
|
+
/** A pointer file's object (`version https://git-lfs.github.com/spec/v1`, `oid sha256:<hex>`, `size <n>`), or null
|
|
11
|
+
* for any other file. */
|
|
12
|
+
export function parseLfsPointer(file) {
|
|
13
|
+
const text = typeof file === 'string' ? file : file.length > 1024 ? '' : new TextDecoder().decode(file);
|
|
14
|
+
if (!text.startsWith('version https://git-lfs.github.com/spec/'))
|
|
15
|
+
return null;
|
|
16
|
+
const oid = /^oid sha256:([0-9a-f]{64})$/m.exec(text)?.[1];
|
|
17
|
+
const size = /^size (\d+)$/m.exec(text)?.[1];
|
|
18
|
+
return oid && size ? { oid, size: Number(size) } : null;
|
|
19
|
+
}
|
|
20
|
+
/** A pointer file's text for an object. */
|
|
21
|
+
export const lfsPointer = (o) => `version https://git-lfs.github.com/spec/v1\noid sha256:${o.oid}\nsize ${o.size}\n`;
|
|
22
|
+
/** An object's sha256, as its oid. */
|
|
23
|
+
export async function lfsOid(bytes) {
|
|
24
|
+
const digest = await globalThis.crypto.subtle.digest('SHA-256', bytes);
|
|
25
|
+
return Array.from(new Uint8Array(digest), (b) => b.toString(16).padStart(2, '0')).join('');
|
|
26
|
+
}
|
|
27
|
+
/** `POST …/info/lfs/objects/batch`: for each object asked, an upload's action (none when the object is held already) or
|
|
28
|
+
* a download's (an error when it is not held), by the basic transfer. `href` names where an object goes up or comes
|
|
29
|
+
* down; `header` is sent with each action. */
|
|
30
|
+
export async function lfsBatch(body, opts) {
|
|
31
|
+
const b = (body && typeof body === 'object' ? body : {});
|
|
32
|
+
// source: https://github.com/git-lfs/git-lfs/blob/main/docs/api/batch.md "Should be download or upload."
|
|
33
|
+
if (b.operation !== 'upload' && b.operation !== 'download')
|
|
34
|
+
return lfsJson(422, { message: 'operation must be download or upload' });
|
|
35
|
+
if (Array.isArray(b.transfers) && b.transfers.length && !b.transfers.includes('basic'))
|
|
36
|
+
return lfsJson(422, { message: 'no transfer adapter in common: this server speaks basic' });
|
|
37
|
+
if (!Array.isArray(b.objects))
|
|
38
|
+
return lfsJson(422, { message: 'objects is required' });
|
|
39
|
+
const operation = b.operation;
|
|
40
|
+
const objects = [];
|
|
41
|
+
for (const raw of b.objects) {
|
|
42
|
+
const o = { oid: String(raw?.oid ?? ''), size: Number(raw?.size) };
|
|
43
|
+
// source: https://github.com/git-lfs/git-lfs/blob/main/docs/api/batch.md "The object is invalid (for example, an invalid oid or size)."
|
|
44
|
+
if (!OID.test(o.oid) || !Number.isSafeInteger(o.size) || o.size < 0) {
|
|
45
|
+
objects.push({ ...o, error: { code: 422, message: 'Invalid object' } });
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const held = await opts.store.get(o.oid);
|
|
49
|
+
if (operation === 'upload') {
|
|
50
|
+
objects.push(held ? { ...o, authenticated: true } : { ...o, authenticated: true, actions: { upload: { href: opts.href('upload', o), ...(opts.header ? { header: opts.header } : {}), expires_in: 3600 } } });
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
// source: https://github.com/git-lfs/git-lfs/blob/main/docs/api/batch.md "The object does not exist on the server."
|
|
54
|
+
objects.push(held ? { oid: o.oid, size: held.length, authenticated: true, actions: { download: { href: opts.href('download', o), ...(opts.header ? { header: opts.header } : {}), expires_in: 3600 } } } : { ...o, error: { code: 404, message: 'Object does not exist' } });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return lfsJson(200, { transfer: 'basic', objects, hash_algo: 'sha256' });
|
|
58
|
+
}
|
|
59
|
+
/** A basic transfer's upload (`PUT` of the object's bytes): kept when they hash to the oid and have the declared size
|
|
60
|
+
* (the size is the request's Content-Length when the caller has no other), else refused and nothing kept. */
|
|
61
|
+
export async function lfsUpload(bytes, o, store) {
|
|
62
|
+
if (bytes.length !== o.size || (await lfsOid(bytes)) !== o.oid)
|
|
63
|
+
return lfsJson(422, { message: `the object's bytes do not match ${o.oid} (${o.size} bytes)` });
|
|
64
|
+
await store.put(o.oid, bytes);
|
|
65
|
+
return new Response(null, { status: 200 });
|
|
66
|
+
}
|
package/dist/src/git/objects.js
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
// <size>\0<payload>`, zlib-deflated, addressed by the SHA-1 of the uncompressed form — written
|
|
3
3
|
// and read through the active BlobStore under `<prefix>/objects/<ab>/<rest>`, the exact layout
|
|
4
4
|
// a bare repository keeps. Pure JS: SHA-1 through WebCrypto, loose objects stored uncompressed in zlib framing (storeZlib),
|
|
5
|
-
// a pack deflated through the
|
|
5
|
+
// a pack deflated through the kernel's `deflate` (bytes.ts), inflate through ./inflate.ts (it reports consumed bytes, which a packfile
|
|
6
6
|
// reader needs). Bun and workerd alike; no `git` binary anywhere.
|
|
7
7
|
import { inflateZlib } from "./inflate.js";
|
|
8
|
+
import { deflate } from "../bytes.js";
|
|
8
9
|
const ENC = new TextEncoder();
|
|
9
10
|
const DEC = new TextDecoder();
|
|
10
11
|
export async function sha1Hex(bytes) {
|
|
@@ -31,13 +32,7 @@ export function decodeLoose(bytes) {
|
|
|
31
32
|
return { type: type, payload };
|
|
32
33
|
}
|
|
33
34
|
export async function hashObject(type, payload) { return sha1Hex(encodeLoose(type, payload)); }
|
|
34
|
-
export async function deflateZlib(bytes) {
|
|
35
|
-
const cs = new CompressionStream('deflate');
|
|
36
|
-
const w = cs.writable.getWriter();
|
|
37
|
-
void w.write(new Uint8Array(bytes));
|
|
38
|
-
void w.close();
|
|
39
|
-
return new Uint8Array(await new Response(cs.readable).arrayBuffer());
|
|
40
|
-
}
|
|
35
|
+
export async function deflateZlib(bytes) { return deflate(bytes); }
|
|
41
36
|
/** zlib with stored (uncompressed) deflate blocks: what git writes at `core.compression 0`, and what every
|
|
42
37
|
* inflater reads. A loose object is written on every commit a World makes; compressing each one through the
|
|
43
38
|
* asynchronous CompressionStream made a write's cost the stream's round trips, not the object's size. */
|
|
@@ -16,7 +16,9 @@ export declare function readAllPktLines(bytes: Uint8Array): {
|
|
|
16
16
|
export type Service = 'git-upload-pack' | 'git-receive-pack';
|
|
17
17
|
/** `GET info/refs?service=<service>`: the ref advertisement, HEAD first as a symref. */
|
|
18
18
|
export declare function advertisement(service: Service, refs: GitRefs): Uint8Array;
|
|
19
|
-
/** `POST git-upload-pack`: wants and haves in, NAK and (once `done`) the pack out.
|
|
19
|
+
/** `POST git-upload-pack`: wants and haves in, NAK and (once `done`) the pack out. A request with `deepen <n>` is shallow:
|
|
20
|
+
* its answer opens with the shallow-update section (`shallow <sha>` for each boundary commit, `unshallow <sha>` for a
|
|
21
|
+
* commit the client held shallow whose parents it now receives, then a flush), and its pack stops at the boundary. */
|
|
20
22
|
export declare function uploadPack(body: Uint8Array, store: GitObjectStore): Promise<Uint8Array>;
|
|
21
23
|
export type RefCommand = {
|
|
22
24
|
old: string;
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// not advertised; a client that asks for it falls back to v0 on this answer, as git specifies.
|
|
6
6
|
import { decodeCommit, decodeTag, decodeTree } from "./objects.js";
|
|
7
7
|
import { readPack, writePack } from "./pack.js";
|
|
8
|
+
import { gunzip } from "../bytes.js";
|
|
8
9
|
const ENC = new TextEncoder();
|
|
9
10
|
const DEC = new TextDecoder();
|
|
10
11
|
export const ZERO_SHA = '0'.repeat(40);
|
|
@@ -55,7 +56,7 @@ function cat(...parts) { const out = new Uint8Array(parts.reduce((n, p) => n + p
|
|
|
55
56
|
out.set(p, o);
|
|
56
57
|
o += p.length;
|
|
57
58
|
} return out; }
|
|
58
|
-
const CAPS = { 'git-upload-pack': 'ofs-delta no-progress agent=volter-twin', 'git-receive-pack': 'report-status delete-refs ofs-delta agent=volter-twin' };
|
|
59
|
+
const CAPS = { 'git-upload-pack': 'ofs-delta shallow no-progress agent=volter-twin', 'git-receive-pack': 'report-status delete-refs ofs-delta agent=volter-twin' };
|
|
59
60
|
/** `GET info/refs?service=<service>`: the ref advertisement, HEAD first as a symref. */
|
|
60
61
|
export function advertisement(service, refs) {
|
|
61
62
|
const all = refs.list();
|
|
@@ -104,11 +105,47 @@ async function closure(store, roots, seen) {
|
|
|
104
105
|
return out;
|
|
105
106
|
}
|
|
106
107
|
async function reachable(store, roots) { const seen = new Set(); await closure(store, roots, seen); return seen; }
|
|
107
|
-
/**
|
|
108
|
+
/**
|
|
109
|
+
* A shallow request's commits (gitprotocol-pack, "Shallow requests"): every commit within `depth` of a want (a want is
|
|
110
|
+
* depth 1), and the boundary, the commits whose parents the client will not have. The client's own shallow commits it
|
|
111
|
+
* already has are boundaries it keeps unless this walk passes through them.
|
|
112
|
+
*/
|
|
113
|
+
async function deepened(store, wants, depth) {
|
|
114
|
+
const at = new Map();
|
|
115
|
+
const boundary = new Set();
|
|
116
|
+
let frontier = wants.map((sha) => ({ sha, d: 1 }));
|
|
117
|
+
while (frontier.length) {
|
|
118
|
+
const next = [];
|
|
119
|
+
for (const { sha, d } of frontier) {
|
|
120
|
+
if ((at.get(sha) ?? Infinity) <= d)
|
|
121
|
+
continue;
|
|
122
|
+
at.set(sha, d);
|
|
123
|
+
const obj = await store.read(sha);
|
|
124
|
+
if (obj?.type !== 'commit')
|
|
125
|
+
continue;
|
|
126
|
+
const parents = decodeCommit(obj.payload).parents;
|
|
127
|
+
if (d >= depth) {
|
|
128
|
+
if (parents.length)
|
|
129
|
+
boundary.add(sha);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
boundary.delete(sha);
|
|
133
|
+
for (const p of parents)
|
|
134
|
+
next.push({ sha: p, d: d + 1 });
|
|
135
|
+
}
|
|
136
|
+
frontier = next;
|
|
137
|
+
}
|
|
138
|
+
return { commits: [...at.keys()], boundary: [...boundary].filter((sha) => at.get(sha) >= depth) };
|
|
139
|
+
}
|
|
140
|
+
/** `POST git-upload-pack`: wants and haves in, NAK and (once `done`) the pack out. A request with `deepen <n>` is shallow:
|
|
141
|
+
* its answer opens with the shallow-update section (`shallow <sha>` for each boundary commit, `unshallow <sha>` for a
|
|
142
|
+
* commit the client held shallow whose parents it now receives, then a flush), and its pack stops at the boundary. */
|
|
108
143
|
export async function uploadPack(body, store) {
|
|
109
144
|
const { lines } = readAllPktLines(body);
|
|
110
145
|
const wants = lines.filter((l) => l.startsWith('want ')).map((l) => l.slice(5, 45)).filter((s) => SHA.test(s));
|
|
111
146
|
const haves = lines.filter((l) => l.startsWith('have ')).map((l) => l.slice(5, 45)).filter((s) => SHA.test(s));
|
|
147
|
+
const clientShallow = lines.filter((l) => l.startsWith('shallow ')).map((l) => l.slice(8, 48)).filter((s) => SHA.test(s));
|
|
148
|
+
const depth = Number(/^deepen (\d+)/.exec(lines.find((l) => l.startsWith('deepen ')) ?? '')?.[1] ?? 0);
|
|
112
149
|
const done = lines.some((l) => l.trim() === 'done');
|
|
113
150
|
const known = [];
|
|
114
151
|
for (const h of haves)
|
|
@@ -117,11 +154,35 @@ export async function uploadPack(body, store) {
|
|
|
117
154
|
for (const w of wants)
|
|
118
155
|
if (!(await store.has(w)))
|
|
119
156
|
return pktLine(`ERR upload-pack: not our ref ${w}\n`);
|
|
157
|
+
const shallow = depth > 0 ? await deepened(store, wants, depth) : undefined;
|
|
158
|
+
const update = shallow
|
|
159
|
+
? cat(...shallow.boundary.filter((sha) => !clientShallow.includes(sha)).map((sha) => pktLine(`shallow ${sha}\n`)), ...clientShallow.filter((sha) => shallow.commits.includes(sha) && !shallow.boundary.includes(sha)).map((sha) => pktLine(`unshallow ${sha}\n`)), FLUSH)
|
|
160
|
+
: new Uint8Array();
|
|
161
|
+
// a stateless client sends its wants and `deepen` alone first and reads only the shallow section back (git's
|
|
162
|
+
// fetch-pack, find_common); every later request carries haves or `done`, and its answer repeats the section before NAK
|
|
163
|
+
if (!done && shallow && haves.length === 0)
|
|
164
|
+
return update;
|
|
120
165
|
if (!done)
|
|
121
|
-
return pktLine('NAK\n');
|
|
166
|
+
return cat(update, pktLine('NAK\n'));
|
|
122
167
|
const seen = await reachable(store, known);
|
|
123
|
-
|
|
124
|
-
|
|
168
|
+
let objects;
|
|
169
|
+
if (shallow) {
|
|
170
|
+
objects = [];
|
|
171
|
+
for (const sha of shallow.commits) {
|
|
172
|
+
if (seen.has(sha))
|
|
173
|
+
continue;
|
|
174
|
+
seen.add(sha);
|
|
175
|
+
const obj = await store.read(sha);
|
|
176
|
+
if (!obj)
|
|
177
|
+
continue;
|
|
178
|
+
objects.push({ ...obj, sha });
|
|
179
|
+
if (obj.type === 'commit')
|
|
180
|
+
objects.push(...await closure(store, [decodeCommit(obj.payload).tree], seen));
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
else
|
|
184
|
+
objects = await closure(store, wants, seen);
|
|
185
|
+
return cat(update, pktLine('NAK\n'), await writePack(objects));
|
|
125
186
|
}
|
|
126
187
|
/** Bounded ancestry walk: is `ancestor` reachable from `sha` through parents? */
|
|
127
188
|
export async function isAncestor(store, ancestor, sha, limit = 5000) {
|
|
@@ -212,7 +273,7 @@ export async function serveSmartHttp(req, repo, tailPath) {
|
|
|
212
273
|
* the size of the repository, so re-wrapping it in a Request only to read it back is a full copy. */
|
|
213
274
|
export async function serveSmartHttpPost(body, contentEncoding, repo, tailPath) {
|
|
214
275
|
if (contentEncoding.includes('gzip'))
|
|
215
|
-
body =
|
|
276
|
+
body = await gunzip(body);
|
|
216
277
|
const headers = { 'content-type': `application/x-${tailPath}-result`, 'cache-control': 'no-cache' };
|
|
217
278
|
if (tailPath === 'git-upload-pack')
|
|
218
279
|
return new Response((await uploadPack(body, repo.store)), { headers });
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** A resolver of one field, `Type.field`: its parent object, its arguments, and the request's handler context. */
|
|
2
|
+
export type GraphqlResolver<C = unknown> = (source: Record<string, unknown>, args: Record<string, unknown>, ctx: C) => unknown;
|
|
3
|
+
/** What a pack's `semantics/graphql.ts` exports as `graphql`. */
|
|
4
|
+
export type GraphqlPart<C = unknown> = {
|
|
5
|
+
/** resolvers by `Type.field` (`Query.repository`, `PullRequest.state`) */
|
|
6
|
+
resolvers: Record<string, GraphqlResolver<C>>;
|
|
7
|
+
/** per type, a GraphQL field name → the object's field that holds it (`nameWithOwner` → `full_name`) */
|
|
8
|
+
aliases?: Record<string, Record<string, string>>;
|
|
9
|
+
/** per type, the object's fields the default resolver may read as they are */
|
|
10
|
+
stored?: Record<string, string[]>;
|
|
11
|
+
};
|
|
12
|
+
/** An error a resolver answers, with the vendor's `type` for it (GitHub's NOT_FOUND, UNPROCESSABLE). */
|
|
13
|
+
export declare class GraphqlError extends Error {
|
|
14
|
+
readonly type?: string | undefined;
|
|
15
|
+
constructor(message: string, type?: string | undefined);
|
|
16
|
+
}
|
|
17
|
+
/** A GraphQL endpoint over a schema and a pack's part: answers a request's `{query, variables, operationName}`. The
|
|
18
|
+
* context each resolver receives is made per request (the handler context). `unmodeled` answers a query selecting a
|
|
19
|
+
* field nothing models; `mutationRefused`, when given, a mutation the World may not make (a read-only twin). */
|
|
20
|
+
export declare function graphqlEndpoint<C>(options: {
|
|
21
|
+
sdl: string;
|
|
22
|
+
part: GraphqlPart<C>;
|
|
23
|
+
context: (request: Request) => Promise<C>;
|
|
24
|
+
unmodeled: (field: {
|
|
25
|
+
type: string;
|
|
26
|
+
field: string;
|
|
27
|
+
}) => Response;
|
|
28
|
+
mutationRefused?: (request: Request) => Response | undefined;
|
|
29
|
+
}): (request: Request) => Promise<Response>;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// THE GRAPHQL WIRE — a vendor's GraphQL API served from its published schema over the pack's state
|
|
2
|
+
// (docs/contributing/architecture.md, "Other wires: lanes, headers and line protocols": GraphQL). A query is parsed and
|
|
3
|
+
// validated against the vendor's own schema, so an unknown field is the GraphQL error the vendor gives. A field resolves
|
|
4
|
+
// from the pack's resolver when it writes one (`semantics/graphql.ts`), else from the object its parent resolved: the
|
|
5
|
+
// field's own name, its snake_case form, or the pack's alias for it. A list answering a `…Connection` field is a Relay
|
|
6
|
+
// connection, and an enum resolves case-insensitively. The wire takes a query only when every field it selects is
|
|
7
|
+
// modeled; any other is answered by the manifest's `graphql.unmodeled`, naming the first field nothing models. An error
|
|
8
|
+
// a resolver throws (`GraphqlError`) carries its `type`, as GitHub's GraphQL errors do.
|
|
9
|
+
//
|
|
10
|
+
// The `graphql` protocol library is loaded the first time a GraphQL request comes (installed by the pack's package.json,
|
|
11
|
+
// as node-postgres is for the managed database): the kernel's own dependencies stay B1's, and importing the kernel
|
|
12
|
+
// loads nothing.
|
|
13
|
+
/** An error a resolver answers, with the vendor's `type` for it (GitHub's NOT_FOUND, UNPROCESSABLE). */
|
|
14
|
+
export class GraphqlError extends Error {
|
|
15
|
+
type;
|
|
16
|
+
constructor(message, type) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.type = type;
|
|
19
|
+
this.name = 'GraphqlError';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
let lib;
|
|
23
|
+
const graphqlLib = () => (lib ??= import('graphql'));
|
|
24
|
+
const snake = (s) => s.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);
|
|
25
|
+
/** A list as a Relay connection, paged by `first`/`last` and `after`. */
|
|
26
|
+
function connection(items, args) {
|
|
27
|
+
const first = typeof args.first === 'number' ? args.first : undefined;
|
|
28
|
+
const last = typeof args.last === 'number' ? args.last : undefined;
|
|
29
|
+
const after = typeof args.after === 'string' ? Number(atob(args.after)) + 1 : 0;
|
|
30
|
+
let slice = items.slice(after);
|
|
31
|
+
if (first !== undefined)
|
|
32
|
+
slice = slice.slice(0, first);
|
|
33
|
+
if (last !== undefined)
|
|
34
|
+
slice = slice.slice(-last);
|
|
35
|
+
const start = after + (last !== undefined ? Math.max(0, items.slice(after).length - last) : 0);
|
|
36
|
+
const cursor = (i) => btoa(String(start + i));
|
|
37
|
+
return {
|
|
38
|
+
totalCount: items.length, nodes: slice, edges: slice.map((node, i) => ({ node, cursor: cursor(i) })),
|
|
39
|
+
pageInfo: { hasNextPage: start + slice.length < items.length, hasPreviousPage: start > 0, startCursor: slice.length ? cursor(0) : null, endCursor: slice.length ? cursor(slice.length - 1) : null },
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/** A GraphQL endpoint over a schema and a pack's part: answers a request's `{query, variables, operationName}`. The
|
|
43
|
+
* context each resolver receives is made per request (the handler context). `unmodeled` answers a query selecting a
|
|
44
|
+
* field nothing models; `mutationRefused`, when given, a mutation the World may not make (a read-only twin). */
|
|
45
|
+
export function graphqlEndpoint(options) {
|
|
46
|
+
let schema;
|
|
47
|
+
const { resolvers, aliases, stored } = options.part;
|
|
48
|
+
const modeled = (type, field) => field === '__typename' || `${type}.${field}` in resolvers || !!aliases?.[type]?.[field] || !!stored?.[type]?.includes(field) || !!stored?.[type]?.includes(snake(field));
|
|
49
|
+
return async (request) => {
|
|
50
|
+
const g = await graphqlLib();
|
|
51
|
+
schema ??= g.buildSchema(options.sdl, { assumeValidSDL: true });
|
|
52
|
+
const payload = (await request.clone().json().catch(() => ({})));
|
|
53
|
+
let document;
|
|
54
|
+
try {
|
|
55
|
+
document = g.parse(payload.query ?? '');
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
return Response.json({ errors: [{ message: error instanceof Error ? error.message : String(error) }] });
|
|
59
|
+
}
|
|
60
|
+
const invalid = g.validate(schema, document);
|
|
61
|
+
if (invalid.length)
|
|
62
|
+
return Response.json({ errors: invalid.map((e) => ({ message: e.message, locations: e.locations })) });
|
|
63
|
+
const mutation = document.definitions.some((d) => d.kind === 'OperationDefinition' && d.operation === 'mutation');
|
|
64
|
+
if (mutation) {
|
|
65
|
+
const refused = options.mutationRefused?.(request);
|
|
66
|
+
if (refused)
|
|
67
|
+
return refused;
|
|
68
|
+
}
|
|
69
|
+
const typeInfo = new g.TypeInfo(schema);
|
|
70
|
+
let unmodeled;
|
|
71
|
+
g.visit(document, g.visitWithTypeInfo(typeInfo, {
|
|
72
|
+
Field(node) {
|
|
73
|
+
const parent = typeInfo.getParentType();
|
|
74
|
+
if (!unmodeled && parent && g.isObjectType(parent) && !parent.name.endsWith('Connection') && !parent.name.endsWith('Edge') && parent.name !== 'PageInfo' && !modeled(parent.name, node.name.value))
|
|
75
|
+
unmodeled = { type: parent.name, field: node.name.value };
|
|
76
|
+
},
|
|
77
|
+
}));
|
|
78
|
+
if (unmodeled)
|
|
79
|
+
return options.unmodeled(unmodeled);
|
|
80
|
+
const ctx = await options.context(request);
|
|
81
|
+
const result = await g.execute({
|
|
82
|
+
schema, document, variableValues: payload.variables ?? {}, ...(payload.operationName ? { operationName: payload.operationName } : {}), contextValue: ctx,
|
|
83
|
+
fieldResolver: (source, args, context, info) => {
|
|
84
|
+
const own = resolvers[`${info.parentType.name}.${info.fieldName}`];
|
|
85
|
+
const obj = (source ?? {});
|
|
86
|
+
let value = own ? own(obj, args, context) : (obj[aliases?.[info.parentType.name]?.[info.fieldName] ?? info.fieldName] ?? obj[snake(info.fieldName)] ?? null);
|
|
87
|
+
const named = g.getNamedType(info.returnType);
|
|
88
|
+
if (Array.isArray(value) && g.isObjectType(named) && named.name.endsWith('Connection'))
|
|
89
|
+
value = connection(value, args);
|
|
90
|
+
if (typeof value === 'string' && g.isEnumType(named))
|
|
91
|
+
value = named.getValues().find((v) => v.name.toLowerCase() === String(value).toLowerCase())?.name ?? value;
|
|
92
|
+
return value;
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
const errors = (result.errors ?? []).map((e) => {
|
|
96
|
+
const type = e.originalError instanceof GraphqlError ? e.originalError.type : typeof e.extensions?.type === 'string' ? e.extensions.type : undefined;
|
|
97
|
+
return { ...(type ? { type } : {}), message: e.message, ...(e.path ? { path: e.path } : {}), ...(e.locations ? { locations: e.locations } : {}) };
|
|
98
|
+
});
|
|
99
|
+
return Response.json({ ...(result.data !== undefined ? { data: result.data } : {}), ...(errors.length ? { errors } : {}) });
|
|
100
|
+
};
|
|
101
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { type H2Handler } from './h2.js';
|
|
2
|
+
import { type ProtoSchema } from './protobuf.js';
|
|
3
|
+
/** gRPC's status codes (https://grpc.github.io/grpc/core/md_doc_statuscodes.html). */
|
|
4
|
+
export declare const GrpcStatus: {
|
|
5
|
+
readonly OK: 0;
|
|
6
|
+
readonly CANCELLED: 1;
|
|
7
|
+
readonly UNKNOWN: 2;
|
|
8
|
+
readonly INVALID_ARGUMENT: 3;
|
|
9
|
+
readonly DEADLINE_EXCEEDED: 4;
|
|
10
|
+
readonly NOT_FOUND: 5;
|
|
11
|
+
readonly ALREADY_EXISTS: 6;
|
|
12
|
+
readonly PERMISSION_DENIED: 7;
|
|
13
|
+
readonly RESOURCE_EXHAUSTED: 8;
|
|
14
|
+
readonly FAILED_PRECONDITION: 9;
|
|
15
|
+
readonly ABORTED: 10;
|
|
16
|
+
readonly OUT_OF_RANGE: 11;
|
|
17
|
+
readonly UNIMPLEMENTED: 12;
|
|
18
|
+
readonly INTERNAL: 13;
|
|
19
|
+
readonly UNAVAILABLE: 14;
|
|
20
|
+
readonly DATA_LOSS: 15;
|
|
21
|
+
readonly UNAUTHENTICATED: 16;
|
|
22
|
+
};
|
|
23
|
+
/** A call answered with a gRPC status other than OK: its code and its message. */
|
|
24
|
+
export declare class GrpcError extends Error {
|
|
25
|
+
readonly code: number;
|
|
26
|
+
constructor(code: number, message: string);
|
|
27
|
+
}
|
|
28
|
+
export type GrpcCall = {
|
|
29
|
+
/** the operation the rpc is (the derive's name for it) and its path */
|
|
30
|
+
operation: string;
|
|
31
|
+
path: string;
|
|
32
|
+
/** the request message, in its proto3 JSON form */
|
|
33
|
+
request: Record<string, unknown>;
|
|
34
|
+
/** the call's metadata (its non-pseudo headers: `authorization`, `x-goog-api-client`, …) */
|
|
35
|
+
metadata: Record<string, string>;
|
|
36
|
+
/** aborted when the client cancels or its deadline passes */
|
|
37
|
+
signal: AbortSignal;
|
|
38
|
+
};
|
|
39
|
+
/** Answers one unary call with its response message's JSON form, or throws a GrpcError. */
|
|
40
|
+
export type GrpcHandler = (call: GrpcCall) => Promise<Record<string, unknown>>;
|
|
41
|
+
/** `grpc-timeout`: an integer and a unit (H M S m u n), PROTOCOL-HTTP2.md "Timeout". */
|
|
42
|
+
export declare function parseGrpcTimeout(value: string | undefined): number | undefined;
|
|
43
|
+
/** Length-prefixed messages out of a request body: [compressed flag][u32 length][bytes]… */
|
|
44
|
+
export declare function splitGrpcFrames(body: Uint8Array): Array<{
|
|
45
|
+
compressed: boolean;
|
|
46
|
+
message: Uint8Array;
|
|
47
|
+
}>;
|
|
48
|
+
export declare function grpcFrame(message: Uint8Array): Uint8Array;
|
|
49
|
+
/** grpc-message is percent-encoded (PROTOCOL-HTTP2.md "Status-Message"). */
|
|
50
|
+
export declare function encodeGrpcMessage(text: string): string;
|
|
51
|
+
/** The HTTP/2 stream handler that answers the unary rpcs of a proto unit's schema through `handler`. */
|
|
52
|
+
export declare function grpcStreamHandler(schema: ProtoSchema, handler: GrpcHandler): H2Handler;
|
|
53
|
+
/** A gRPC call answered by the pack's own fetch, as the derive serves a proto unit's rpc (`POST /<package>.<Service>/
|
|
54
|
+
* <Method>`, the request message's JSON form the body): its JSON answer is the response message; any other status its
|
|
55
|
+
* gRPC code, the error's message as the status message. */
|
|
56
|
+
export declare function grpcOverFetch(fetch: (request: Request) => Promise<Response>): GrpcHandler;
|
|
57
|
+
/** A pack that speaks gRPC, served on one port: HTTP/2 (gRPC, by the schema through the pack's fetch) and HTTP/1 (the
|
|
58
|
+
* pack's fetch: its doors, its REST) on the same socket, as the kernel's HTTP/2 server tells them apart. Inside a World
|
|
59
|
+
* it answers the World's boot probe as `serveHttp` does. */
|
|
60
|
+
export declare function serveGrpc(options: {
|
|
61
|
+
port?: number;
|
|
62
|
+
fetch: (request: Request) => Promise<Response>;
|
|
63
|
+
schema: ProtoSchema;
|
|
64
|
+
}): Promise<{
|
|
65
|
+
port: number;
|
|
66
|
+
stop: () => void;
|
|
67
|
+
}>;
|