@loopbacktools/server 0.0.0-stage → 0.4.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/LICENSE +21 -0
- package/README.md +26 -2
- package/dist/package.json +57 -0
- package/dist/src/domain/ask.d.ts +78 -0
- package/dist/src/domain/ask.js +161 -0
- package/dist/src/domain/errors.d.ts +70 -0
- package/dist/src/domain/errors.js +57 -0
- package/dist/src/generated/app.gen.d.ts +2311 -0
- package/dist/src/generated/app.gen.js +400 -0
- package/dist/src/generated/runtime.gen.d.ts +357 -0
- package/dist/src/generated/runtime.gen.js +298 -0
- package/dist/src/generated/schemas.gen.d.ts +6416 -0
- package/dist/src/generated/schemas.gen.js +306 -0
- package/dist/src/generated/wire-contract.gen.d.ts +77 -0
- package/dist/src/generated/wire-contract.gen.js +3 -0
- package/dist/src/index.d.ts +11 -0
- package/dist/src/index.js +8 -0
- package/dist/src/loopback.d.ts +27 -0
- package/dist/src/loopback.js +16 -0
- package/dist/src/space/arguments.d.ts +8 -0
- package/dist/src/space/arguments.js +52 -0
- package/dist/src/space/migrations.d.ts +10 -0
- package/dist/src/space/migrations.js +106 -0
- package/dist/src/space/object.d.ts +30 -0
- package/dist/src/space/object.js +169 -0
- package/dist/src/space/operations.d.ts +149 -0
- package/dist/src/space/operations.js +568 -0
- package/dist/src/space/read.d.ts +8 -0
- package/dist/src/space/read.js +33 -0
- package/dist/src/space/rpc.d.ts +61 -0
- package/dist/src/space/rpc.js +96 -0
- package/dist/src/space/services.d.ts +41 -0
- package/dist/src/space/services.js +58 -0
- package/dist/src/space/store.d.ts +87 -0
- package/dist/src/space/store.js +276 -0
- package/dist/src/version.d.ts +2 -0
- package/dist/src/version.js +3 -0
- package/dist/src/worker/env.d.ts +19 -0
- package/dist/src/worker/env.js +3 -0
- package/dist/src/worker/files.d.ts +21 -0
- package/dist/src/worker/files.js +42 -0
- package/dist/src/worker/gate.d.ts +55 -0
- package/dist/src/worker/gate.js +83 -0
- package/dist/src/worker/http.d.ts +2092 -0
- package/dist/src/worker/http.js +291 -0
- package/dist/src/worker/jwt.d.ts +49 -0
- package/dist/src/worker/jwt.js +142 -0
- package/dist/src/worker/mcp.d.ts +15 -0
- package/dist/src/worker/mcp.js +401 -0
- package/dist/src/worker/run.d.ts +45 -0
- package/dist/src/worker/run.js +71 -0
- package/dist/src/worker/spaces.d.ts +24 -0
- package/dist/src/worker/spaces.js +64 -0
- package/package.json +51 -3
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Context, Effect, Layer } from "effect";
|
|
2
|
+
import { Unauthorized } from "../domain/errors.ts";
|
|
3
|
+
/** Who a request is from, and which space it opens. */
|
|
4
|
+
export interface Access {
|
|
5
|
+
readonly space: string;
|
|
6
|
+
readonly actor: {
|
|
7
|
+
readonly id: string;
|
|
8
|
+
readonly name?: string;
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
declare const Gate_base: Context.ServiceClass<Gate, "loopback/Gate", {
|
|
12
|
+
readonly authenticate: (request: Request) => Effect.Effect<Access, Unauthorized>;
|
|
13
|
+
/**
|
|
14
|
+
* The OAuth authorization servers that issue the credentials this gate accepts, which MCP clients
|
|
15
|
+
* discover through protected resource metadata (RFC 9728). None when the gate has none.
|
|
16
|
+
*/
|
|
17
|
+
readonly authorizationServers: Effect.Effect<ReadonlyArray<string>>;
|
|
18
|
+
}>;
|
|
19
|
+
/**
|
|
20
|
+
* Decides who a request is from when no invoker has vouched for it. This is where a deployment plugs
|
|
21
|
+
* in: the token gate below serves a single space behind one secret, the JWT gate serves a space per
|
|
22
|
+
* tenant behind any login that issues JWTs, and any other gate (an edge identity header, say) is
|
|
23
|
+
* another layer providing this service.
|
|
24
|
+
*/
|
|
25
|
+
export declare class Gate extends Gate_base {
|
|
26
|
+
}
|
|
27
|
+
/** A deployment that cannot serve any request until it is configured. Answered as a 500 that says why. */
|
|
28
|
+
export declare class NotConfigured extends Error {
|
|
29
|
+
readonly name = "NotConfigured";
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The access a Worker's invoker vouches for, when it does: `props.loopback`, as another Worker
|
|
33
|
+
* passes it through a service binding in `ctx.props`. Only code deployed on the
|
|
34
|
+
* account can set props, so nothing on the network can forge them, and they need no secret. Props
|
|
35
|
+
* that say something else are a misconfigured invoker: refused, never passed over for the gate.
|
|
36
|
+
*/
|
|
37
|
+
export declare const vouchedAccess: (props: unknown) => Effect.Effect<Access | undefined>;
|
|
38
|
+
/**
|
|
39
|
+
* The bearer token a request presents. Only ever a header: Cloudflare redacts `Authorization` from
|
|
40
|
+
* its logs, while a token sent any other way (a WebSocket subprotocol, say) would be logged in full.
|
|
41
|
+
* Browsers open the event socket with a one-time ticket instead.
|
|
42
|
+
*/
|
|
43
|
+
export declare const credentialOf: (request: Request) => string | undefined;
|
|
44
|
+
/** The space a token-gated deployment serves. */
|
|
45
|
+
export declare const DEFAULT_SPACE = "default";
|
|
46
|
+
/**
|
|
47
|
+
* One secret, `LOOPBACK_TOKEN`, opens the deployment's single space. Compared in constant time over
|
|
48
|
+
* digests, so neither the token's content nor its length leaks through timing. The secret is read,
|
|
49
|
+
* as a redacted value no log can show, and digested once per isolate on first use: a deployment
|
|
50
|
+
* whose invoker vouches for every caller never needs it.
|
|
51
|
+
*/
|
|
52
|
+
export declare const TokenGate: Layer.Layer<Gate, never, never>;
|
|
53
|
+
/** The token gate's service, for a gate that chooses between gates. */
|
|
54
|
+
export declare const makeTokenGate: Effect.Effect<Gate["Service"]>;
|
|
55
|
+
export {};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { Config, Context, Effect, Layer, Redacted } from "effect";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { Unauthorized } from "../domain/errors.js";
|
|
4
|
+
/**
|
|
5
|
+
* Decides who a request is from when no invoker has vouched for it. This is where a deployment plugs
|
|
6
|
+
* in: the token gate below serves a single space behind one secret, the JWT gate serves a space per
|
|
7
|
+
* tenant behind any login that issues JWTs, and any other gate (an edge identity header, say) is
|
|
8
|
+
* another layer providing this service.
|
|
9
|
+
*/
|
|
10
|
+
export class Gate extends Context.Service()("loopback/Gate") {
|
|
11
|
+
}
|
|
12
|
+
/** A deployment that cannot serve any request until it is configured. Answered as a 500 that says why. */
|
|
13
|
+
export class NotConfigured extends Error {
|
|
14
|
+
name = "NotConfigured";
|
|
15
|
+
}
|
|
16
|
+
const vouched = z.strictObject({
|
|
17
|
+
space: z.string().min(1),
|
|
18
|
+
actor: z.strictObject({ id: z.string().min(1), name: z.string().min(1).optional() }),
|
|
19
|
+
});
|
|
20
|
+
/**
|
|
21
|
+
* The access a Worker's invoker vouches for, when it does: `props.loopback`, as another Worker
|
|
22
|
+
* passes it through a service binding in `ctx.props`. Only code deployed on the
|
|
23
|
+
* account can set props, so nothing on the network can forge them, and they need no secret. Props
|
|
24
|
+
* that say something else are a misconfigured invoker: refused, never passed over for the gate.
|
|
25
|
+
*/
|
|
26
|
+
export const vouchedAccess = (props) => {
|
|
27
|
+
const claim = props?.loopback;
|
|
28
|
+
if (claim === undefined)
|
|
29
|
+
return Effect.succeed(undefined);
|
|
30
|
+
const parsed = vouched.safeParse(claim);
|
|
31
|
+
if (!parsed.success) {
|
|
32
|
+
return Effect.die(new NotConfigured("The invoker's props.loopback must be { space, actor: { id, name? } }."));
|
|
33
|
+
}
|
|
34
|
+
const { space, actor } = parsed.data;
|
|
35
|
+
return Effect.succeed({
|
|
36
|
+
space,
|
|
37
|
+
actor: actor.name === undefined ? { id: actor.id } : { id: actor.id, name: actor.name },
|
|
38
|
+
});
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* The bearer token a request presents. Only ever a header: Cloudflare redacts `Authorization` from
|
|
42
|
+
* its logs, while a token sent any other way (a WebSocket subprotocol, say) would be logged in full.
|
|
43
|
+
* Browsers open the event socket with a one-time ticket instead.
|
|
44
|
+
*/
|
|
45
|
+
export const credentialOf = (request) => /^Bearer\s+(\S+)$/i.exec(request.headers.get("Authorization") ?? "")?.[1];
|
|
46
|
+
const digest = (text) => crypto.subtle.digest("SHA-256", new TextEncoder().encode(text));
|
|
47
|
+
/** The space a token-gated deployment serves. */
|
|
48
|
+
export const DEFAULT_SPACE = "default";
|
|
49
|
+
/** The shortest token accepted: long enough that guessing it is hopeless. */
|
|
50
|
+
const MIN_TOKEN_LENGTH = 32;
|
|
51
|
+
/**
|
|
52
|
+
* One secret, `LOOPBACK_TOKEN`, opens the deployment's single space. Compared in constant time over
|
|
53
|
+
* digests, so neither the token's content nor its length leaks through timing. The secret is read,
|
|
54
|
+
* as a redacted value no log can show, and digested once per isolate on first use: a deployment
|
|
55
|
+
* whose invoker vouches for every caller never needs it.
|
|
56
|
+
*/
|
|
57
|
+
export const TokenGate = Layer.effect(Gate, Effect.suspend(() => makeTokenGate));
|
|
58
|
+
/** The token gate's service, for a gate that chooses between gates. */
|
|
59
|
+
export const makeTokenGate = Effect.gen(function* () {
|
|
60
|
+
const expected = yield* Effect.cached(Effect.gen(function* () {
|
|
61
|
+
const secret = yield* Config.Redacted("LOOPBACK_TOKEN").pipe(Effect.catch(() => Effect.die(new NotConfigured("LOOPBACK_TOKEN is not set. Deploy with it in a secrets file (`cf deploy --secrets-file <file>`), or set LOOPBACK_JWKS_URL and the other JWT settings for a space per tenant."))));
|
|
62
|
+
if (Redacted.value(secret).length < MIN_TOKEN_LENGTH) {
|
|
63
|
+
return yield* Effect.die(new NotConfigured(`LOOPBACK_TOKEN must be at least ${MIN_TOKEN_LENGTH} characters. Generate one with \`openssl rand -hex 32\`.`));
|
|
64
|
+
}
|
|
65
|
+
return yield* Effect.promise(() => digest(Redacted.value(secret)));
|
|
66
|
+
}));
|
|
67
|
+
return Gate.of({
|
|
68
|
+
authenticate: (request) => Effect.gen(function* () {
|
|
69
|
+
const presented = credentialOf(request);
|
|
70
|
+
if (presented === undefined) {
|
|
71
|
+
return yield* new Unauthorized({ reason: "A bearer token is required." });
|
|
72
|
+
}
|
|
73
|
+
const wanted = yield* expected;
|
|
74
|
+
const actual = yield* Effect.promise(() => digest(presented));
|
|
75
|
+
if (!crypto.subtle.timingSafeEqual(wanted, actual)) {
|
|
76
|
+
return yield* new Unauthorized({ reason: "The token is not valid." });
|
|
77
|
+
}
|
|
78
|
+
const access = { space: DEFAULT_SPACE, actor: { id: "owner" } };
|
|
79
|
+
return access;
|
|
80
|
+
}),
|
|
81
|
+
authorizationServers: Effect.succeed([]),
|
|
82
|
+
});
|
|
83
|
+
});
|