@wervt/nuxt 0.3.2 → 0.3.3
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 +2 -0
- package/dist/dev.d.ts +3 -1
- package/dist/env-types.js +14 -0
- package/dist/icons.js +31 -0
- package/dist/module.js +39 -0
- package/dist/node_modules/.pnpm/nitro@3.0.260903-beta/node_modules/nitro/dist/runtime/task.js +1 -2
- package/dist/og-image.js +39 -0
- package/dist/provision.d.ts +19 -2
- package/dist/provision.js +49 -2
- package/dist/runtime/app/snapshot.d.ts +17 -0
- package/dist/runtime/app/snapshot.js +64 -0
- package/dist/runtime/app/useShape.d.ts +8 -1
- package/dist/runtime/app/useShape.js +8 -52
- package/dist/runtime/app/useWervtFeedback.d.ts +2 -1
- package/dist/runtime/email/index.d.ts +2 -1
- package/dist/runtime/email/index.js +2 -1
- package/dist/runtime/email/invite.d.ts +75 -0
- package/dist/runtime/email/invite.js +60 -0
- package/dist/runtime/server/access.d.ts +3 -2
- package/dist/runtime/server/access.js +3 -2
- package/dist/runtime/server/agent-token-plugin.d.ts +13 -0
- package/dist/runtime/server/agent-token-plugin.js +10 -0
- package/dist/runtime/server/anthropic.d.ts +21 -0
- package/dist/runtime/server/anthropic.js +53 -0
- package/dist/runtime/server/identity.d.ts +20 -0
- package/dist/runtime/server/identity.js +37 -0
- package/dist/runtime/server/index.d.ts +4 -3
- package/dist/runtime/server/index.js +4 -3
- package/dist/runtime/server/task-handlers.d.ts +2 -1
- package/dist/runtime/server/tools.d.ts +2 -1
- package/dist/runtime/server/user.d.ts +9 -3
- package/dist/runtime/server/user.js +28 -3
- package/dist/runtime/server/validate.d.ts +2 -1
- package/dist/runtime/shared/agent-token.d.ts +9 -0
- package/dist/runtime/shared/agent-token.js +17 -0
- package/dist/runtime/shared/shape.d.ts +2 -1
- package/dist/runtime/shared/user.d.ts +8 -0
- package/dist/runtime/shared/user.js +4 -1
- package/dist/storage.d.ts +48 -1
- package/dist/storage.js +217 -3
- package/package.json +15 -6
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { EmailButton, EmailHeading, EmailMuted, EmailText, WervtEmail } from "./components.js";
|
|
2
|
+
import { defineComponent, h } from "vue";
|
|
3
|
+
//#region src/runtime/email/invite.ts
|
|
4
|
+
/**
|
|
5
|
+
* An invitation to an app: who invited, where to go, and that signing in with this address is
|
|
6
|
+
* all it takes. Send it with `sendMail({ to, template: InviteEmail, props: { by, app, url } })`.
|
|
7
|
+
*/
|
|
8
|
+
const InviteEmail = defineComponent({
|
|
9
|
+
name: "InviteEmail",
|
|
10
|
+
props: {
|
|
11
|
+
/** Who invited them, by name or email. */
|
|
12
|
+
by: {
|
|
13
|
+
type: String,
|
|
14
|
+
required: true
|
|
15
|
+
},
|
|
16
|
+
/** The app they're invited to, as people know it. */
|
|
17
|
+
app: {
|
|
18
|
+
type: String,
|
|
19
|
+
required: true
|
|
20
|
+
},
|
|
21
|
+
/** Where the button goes: the app, or a link that lets them in. */
|
|
22
|
+
url: {
|
|
23
|
+
type: String,
|
|
24
|
+
required: true
|
|
25
|
+
},
|
|
26
|
+
/** What the app is, in a sentence after "<by> invited you to <app>." */
|
|
27
|
+
message: {
|
|
28
|
+
type: String,
|
|
29
|
+
default: ""
|
|
30
|
+
},
|
|
31
|
+
/** The button's label; "Open <app>" unless set. */
|
|
32
|
+
action: {
|
|
33
|
+
type: String,
|
|
34
|
+
default: ""
|
|
35
|
+
},
|
|
36
|
+
/** A hint under the button; by default, to sign in with this address. */
|
|
37
|
+
note: {
|
|
38
|
+
type: String,
|
|
39
|
+
default: ""
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
setup(props) {
|
|
43
|
+
return () => h(WervtEmail, {
|
|
44
|
+
subject: `${props.by} invited you to ${props.app}`,
|
|
45
|
+
app: props.app,
|
|
46
|
+
preview: props.message || `Join ${props.app}`,
|
|
47
|
+
reason: `You received this email because ${props.by} invited this address.`
|
|
48
|
+
}, () => [
|
|
49
|
+
h(EmailHeading, { eyebrow: "Invitation" }, () => `You're invited to ${props.app}`),
|
|
50
|
+
h(EmailText, () => [`${props.by} invited you to ${props.app}.`, props.message].filter(Boolean).join(" ")),
|
|
51
|
+
h(EmailButton, {
|
|
52
|
+
href: props.url,
|
|
53
|
+
color: "neutral"
|
|
54
|
+
}, () => props.action || `Open ${props.app}`),
|
|
55
|
+
h(EmailMuted, () => props.note || "Sign in with this email address to get in.")
|
|
56
|
+
]);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
//#endregion
|
|
60
|
+
export { InviteEmail };
|
|
@@ -24,9 +24,10 @@ export interface WervtAccess {
|
|
|
24
24
|
/**
|
|
25
25
|
* Invites on an invite-only app (`wervt access <app> --invite`), as the user of this request: the
|
|
26
26
|
* runtime vouches for them, and only someone with access can invite. Everyone with access may
|
|
27
|
-
* invite
|
|
27
|
+
* invite, or only the owner, members and admins with `wervt access <app> --invite-by owners`;
|
|
28
|
+
* restrict it further in the route if the app has its own roles.
|
|
28
29
|
*
|
|
29
|
-
* Throws with the platform's status: 401 without a signed-in user, 403 when they
|
|
30
|
+
* Throws with the platform's status: 401 without a signed-in user, 403 when they may not invite,
|
|
30
31
|
* 409 when the app isn't invite-only, 429 past the limits (25 invites per person and day, 500 per
|
|
31
32
|
* app).
|
|
32
33
|
*/
|
|
@@ -5,9 +5,10 @@ const devInvites = [];
|
|
|
5
5
|
/**
|
|
6
6
|
* Invites on an invite-only app (`wervt access <app> --invite`), as the user of this request: the
|
|
7
7
|
* runtime vouches for them, and only someone with access can invite. Everyone with access may
|
|
8
|
-
* invite
|
|
8
|
+
* invite, or only the owner, members and admins with `wervt access <app> --invite-by owners`;
|
|
9
|
+
* restrict it further in the route if the app has its own roles.
|
|
9
10
|
*
|
|
10
|
-
* Throws with the platform's status: 401 without a signed-in user, 403 when they
|
|
11
|
+
* Throws with the platform's status: 401 without a signed-in user, 403 when they may not invite,
|
|
11
12
|
* 409 when the app isn't invite-only, 429 past the limits (25 invites per person and day, 500 per
|
|
12
13
|
* app).
|
|
13
14
|
*/
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//#region src/runtime/server/agent-token-plugin.d.ts
|
|
2
|
+
interface EnrichContext {
|
|
3
|
+
event: Record<string, unknown>;
|
|
4
|
+
headers?: Record<string, string>;
|
|
5
|
+
}
|
|
6
|
+
interface NitroAppHooks {
|
|
7
|
+
hooks: {
|
|
8
|
+
hook(name: 'evlog:enrich', handler: (ctx: EnrichContext) => void): unknown;
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export default function wervtAgentToken(nitroApp: NitroAppHooks): void;
|
|
12
|
+
//#endregion
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { parseAgentToken } from "../shared/agent-token.js";
|
|
2
|
+
//#region src/runtime/server/agent-token-plugin.ts
|
|
3
|
+
function wervtAgentToken(nitroApp) {
|
|
4
|
+
nitroApp.hooks.hook("evlog:enrich", (ctx) => {
|
|
5
|
+
const agentToken = parseAgentToken(ctx.headers?.["x-wervt-agent-token"]);
|
|
6
|
+
if (agentToken) ctx.event.agentToken = agentToken;
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
//#endregion
|
|
10
|
+
export { wervtAgentToken as default };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
2
|
+
//#region src/runtime/server/anthropic.d.ts
|
|
3
|
+
export interface UseAnthropicOptions {
|
|
4
|
+
/**
|
|
5
|
+
* The key for `nuxt dev`, where there is no workload identity. Default: `ANTHROPIC_API_KEY`.
|
|
6
|
+
* Never used on wervt.
|
|
7
|
+
*/
|
|
8
|
+
devApiKey?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* An Anthropic client that authenticates as this app through its wervt identity: no API key, only
|
|
12
|
+
* IDs from the Anthropic Console in the app's env (`ANTHROPIC_FEDERATION_RULE_ID`,
|
|
13
|
+
* `ANTHROPIC_ORGANIZATION_ID`, optionally `ANTHROPIC_SERVICE_ACCOUNT_ID` and
|
|
14
|
+
* `ANTHROPIC_WORKSPACE_ID`). The SDK trades an identity token for an Anthropic token and renews it
|
|
15
|
+
* before it runs out.
|
|
16
|
+
*
|
|
17
|
+
* Needs `wervt identity enable <app>` and a federation rule for the app's subject
|
|
18
|
+
* (`wervt identity status <app>`). In `nuxt dev` it uses `options.devApiKey` instead.
|
|
19
|
+
*/
|
|
20
|
+
export declare function useAnthropic(options?: UseAnthropicOptions): Anthropic;
|
|
21
|
+
//#endregion
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { useWorkloadIdentity } from "./identity.js";
|
|
2
|
+
import { createError } from "nuxt/server";
|
|
3
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
4
|
+
import { oidcFederationProvider } from "@anthropic-ai/sdk/lib/credentials/oidc-federation";
|
|
5
|
+
//#region src/runtime/server/anthropic.ts
|
|
6
|
+
const BASE_URL = "https://api.anthropic.com";
|
|
7
|
+
let client;
|
|
8
|
+
/**
|
|
9
|
+
* An Anthropic client that authenticates as this app through its wervt identity: no API key, only
|
|
10
|
+
* IDs from the Anthropic Console in the app's env (`ANTHROPIC_FEDERATION_RULE_ID`,
|
|
11
|
+
* `ANTHROPIC_ORGANIZATION_ID`, optionally `ANTHROPIC_SERVICE_ACCOUNT_ID` and
|
|
12
|
+
* `ANTHROPIC_WORKSPACE_ID`). The SDK trades an identity token for an Anthropic token and renews it
|
|
13
|
+
* before it runs out.
|
|
14
|
+
*
|
|
15
|
+
* Needs `wervt identity enable <app>` and a federation rule for the app's subject
|
|
16
|
+
* (`wervt identity status <app>`). In `nuxt dev` it uses `options.devApiKey` instead.
|
|
17
|
+
*/
|
|
18
|
+
function useAnthropic(options = {}) {
|
|
19
|
+
return client ??= create(options);
|
|
20
|
+
}
|
|
21
|
+
function create(options) {
|
|
22
|
+
if (!process.env.WERVT_RUNTIME_URL) {
|
|
23
|
+
const apiKey = options.devApiKey ?? process.env.ANTHROPIC_API_KEY;
|
|
24
|
+
if (!apiKey) throw createError({
|
|
25
|
+
status: 503,
|
|
26
|
+
message: "useAnthropic() in `nuxt dev` needs an API key (ANTHROPIC_API_KEY or devApiKey)."
|
|
27
|
+
});
|
|
28
|
+
return new Anthropic({ apiKey });
|
|
29
|
+
}
|
|
30
|
+
const federationRuleId = process.env.ANTHROPIC_FEDERATION_RULE_ID;
|
|
31
|
+
const organizationId = process.env.ANTHROPIC_ORGANIZATION_ID;
|
|
32
|
+
if (!federationRuleId || !organizationId) throw createError({
|
|
33
|
+
status: 503,
|
|
34
|
+
message: "useAnthropic() needs ANTHROPIC_FEDERATION_RULE_ID and ANTHROPIC_ORGANIZATION_ID (wervt env <app> --set …)."
|
|
35
|
+
});
|
|
36
|
+
const identity = useWorkloadIdentity();
|
|
37
|
+
return new Anthropic({
|
|
38
|
+
apiKey: null,
|
|
39
|
+
authToken: null,
|
|
40
|
+
baseURL: BASE_URL,
|
|
41
|
+
credentials: oidcFederationProvider({
|
|
42
|
+
identityTokenProvider: () => identity.token(BASE_URL),
|
|
43
|
+
federationRuleId,
|
|
44
|
+
organizationId,
|
|
45
|
+
serviceAccountId: process.env.ANTHROPIC_SERVICE_ACCOUNT_ID || void 0,
|
|
46
|
+
workspaceId: process.env.ANTHROPIC_WORKSPACE_ID || void 0,
|
|
47
|
+
baseURL: BASE_URL,
|
|
48
|
+
fetch: (...args) => fetch(...args)
|
|
49
|
+
})
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
//#endregion
|
|
53
|
+
export { useAnthropic };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
//#region src/runtime/server/identity.d.ts
|
|
2
|
+
export interface WorkloadIdentity {
|
|
3
|
+
/**
|
|
4
|
+
* A fresh JWT that says "I am this wervt app", for `audience` (a provider on wervt's list, e.g.
|
|
5
|
+
* `https://api.anthropic.com`). Valid for 5 minutes and single-use: trade it for a provider
|
|
6
|
+
* token right away. Each call signs a new one; providers' SDKs cache what they get for it.
|
|
7
|
+
*/
|
|
8
|
+
token(audience: string): Promise<string>;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* The app's workload identity: tokens signed by wervt's issuer, so the app can authenticate to an
|
|
12
|
+
* AI provider (Anthropic's Workload Identity Federation) without an API key. Its owner turns it
|
|
13
|
+
* on with `wervt identity enable <app>`; `useAnthropic()` (`@wervt/nuxt/anthropic`) uses it.
|
|
14
|
+
*
|
|
15
|
+
* Throws with the platform's status: 403 when the app has no identity, 400 for an audience wervt
|
|
16
|
+
* doesn't sign for, 429 past 10 tokens a minute, 503 when the issuer is off. In `nuxt dev` there
|
|
17
|
+
* is no issuer, so it always throws.
|
|
18
|
+
*/
|
|
19
|
+
export declare function useWorkloadIdentity(): WorkloadIdentity;
|
|
20
|
+
//#endregion
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { createError } from "nuxt/server";
|
|
2
|
+
//#region src/runtime/server/identity.ts
|
|
3
|
+
/**
|
|
4
|
+
* The app's workload identity: tokens signed by wervt's issuer, so the app can authenticate to an
|
|
5
|
+
* AI provider (Anthropic's Workload Identity Federation) without an API key. Its owner turns it
|
|
6
|
+
* on with `wervt identity enable <app>`; `useAnthropic()` (`@wervt/nuxt/anthropic`) uses it.
|
|
7
|
+
*
|
|
8
|
+
* Throws with the platform's status: 403 when the app has no identity, 400 for an audience wervt
|
|
9
|
+
* doesn't sign for, 429 past 10 tokens a minute, 503 when the issuer is off. In `nuxt dev` there
|
|
10
|
+
* is no issuer, so it always throws.
|
|
11
|
+
*/
|
|
12
|
+
function useWorkloadIdentity() {
|
|
13
|
+
return { async token(audience) {
|
|
14
|
+
const runtime = process.env.WERVT_RUNTIME_URL;
|
|
15
|
+
if (!runtime) throw createError({
|
|
16
|
+
status: 503,
|
|
17
|
+
message: "Workload identity only exists on wervt, not in `nuxt dev`."
|
|
18
|
+
});
|
|
19
|
+
const res = await fetch(new URL("/_wervt/identity-token", runtime), {
|
|
20
|
+
method: "POST",
|
|
21
|
+
headers: {
|
|
22
|
+
"content-type": "application/json",
|
|
23
|
+
"x-wervt-app": process.env.WERVT_APP ?? "",
|
|
24
|
+
"x-wervt-token": process.env.WERVT_TOKEN ?? ""
|
|
25
|
+
},
|
|
26
|
+
body: JSON.stringify({ audience })
|
|
27
|
+
});
|
|
28
|
+
const body = await res.json().catch(() => ({}));
|
|
29
|
+
if (!res.ok || !body.token) throw createError({
|
|
30
|
+
status: res.status,
|
|
31
|
+
message: body.error ?? `identity token failed (${res.status})`
|
|
32
|
+
});
|
|
33
|
+
return body.token;
|
|
34
|
+
} };
|
|
35
|
+
}
|
|
36
|
+
//#endregion
|
|
37
|
+
export { useWorkloadIdentity };
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { WervtUser } from "../shared/user.js";
|
|
1
|
+
import { WervtRole, WervtUser } from "../shared/user.js";
|
|
2
2
|
import { EmailProps, RenderedEmail, renderEmail } from "../email/render.js";
|
|
3
3
|
import { WervtAccess, WervtInvite, useWervtAccess } from "./access.js";
|
|
4
4
|
import { useDb } from "./db.js";
|
|
5
|
+
import { WorkloadIdentity, useWorkloadIdentity } from "./identity.js";
|
|
5
6
|
import { createEvlogError, log, useLogger } from "./logger.js";
|
|
6
7
|
import { SendMailOptions, sendMail } from "./mail.js";
|
|
7
8
|
import { McpTokenContext } from "./mcp-auth.js";
|
|
@@ -11,5 +12,5 @@ import { ShapeOptions, ShapeRow, proxyShape } from "./shape.js";
|
|
|
11
12
|
import { Task, TaskEvent, TaskResult, defineTask } from "./tasks.js";
|
|
12
13
|
import { SafeResult, useSafeValidatedBody, useSafeValidatedParams, useSafeValidatedQuery, useValidatedBody, useValidatedParams, useValidatedQuery, vh } from "./validate.js";
|
|
13
14
|
import { Tool, ToolRoute, defineTool, defineToolRoute } from "./tools.js";
|
|
14
|
-
import { useWervtUser } from "./user.js";
|
|
15
|
-
export { type EmailProps, type McpTokenContext, type RenderedEmail, type SafeResult, type SendMailOptions, type ShapeOptions, type ShapeResponse, type ShapeRow, type Task, type TaskEvent, type TaskResult, type Tool, type ToolRoute, type WervtAccess, type WervtInvite, type WervtUser, createEvlogError, defineTask, defineTool, defineToolRoute, log, proxyShape, renderEmail, seedTable, sendMail, useDb, useLogger, useSafeValidatedBody, useSafeValidatedParams, useSafeValidatedQuery, useValidatedBody, useValidatedParams, useValidatedQuery, useWervtAccess, useWervtUser, vh };
|
|
15
|
+
import { requireWervtRole, useWervtUser } from "./user.js";
|
|
16
|
+
export { type EmailProps, type McpTokenContext, type RenderedEmail, type SafeResult, type SendMailOptions, type ShapeOptions, type ShapeResponse, type ShapeRow, type Task, type TaskEvent, type TaskResult, type Tool, type ToolRoute, type WervtAccess, type WervtInvite, type WervtRole, type WervtUser, type WorkloadIdentity, createEvlogError, defineTask, defineTool, defineToolRoute, log, proxyShape, renderEmail, requireWervtRole, seedTable, sendMail, useDb, useLogger, useSafeValidatedBody, useSafeValidatedParams, useSafeValidatedQuery, useValidatedBody, useValidatedParams, useValidatedQuery, useWervtAccess, useWervtUser, useWorkloadIdentity, vh };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { renderEmail } from "../email/render.js";
|
|
1
2
|
import { useWervtAccess } from "./access.js";
|
|
3
|
+
import { useWorkloadIdentity } from "./identity.js";
|
|
2
4
|
import { useDb } from "./db.js";
|
|
3
|
-
import { renderEmail } from "../email/render.js";
|
|
4
5
|
import { createEvlogError, log, useLogger } from "./logger.js";
|
|
5
6
|
import { sendMail } from "./mail.js";
|
|
6
7
|
import { seedTable } from "./seed.js";
|
|
@@ -8,5 +9,5 @@ import { proxyShape } from "./shape.js";
|
|
|
8
9
|
import { defineTask } from "./tasks.js";
|
|
9
10
|
import { useSafeValidatedBody, useSafeValidatedParams, useSafeValidatedQuery, useValidatedBody, useValidatedParams, useValidatedQuery, vh } from "./validate.js";
|
|
10
11
|
import { defineTool, defineToolRoute } from "./tools.js";
|
|
11
|
-
import { useWervtUser } from "./user.js";
|
|
12
|
-
export { createEvlogError, defineTask, defineTool, defineToolRoute, log, proxyShape, renderEmail, seedTable, sendMail, useDb, useLogger, useSafeValidatedBody, useSafeValidatedParams, useSafeValidatedQuery, useValidatedBody, useValidatedParams, useValidatedQuery, useWervtAccess, useWervtUser, vh };
|
|
12
|
+
import { requireWervtRole, useWervtUser } from "./user.js";
|
|
13
|
+
export { createEvlogError, defineTask, defineTool, defineToolRoute, log, proxyShape, renderEmail, requireWervtRole, seedTable, sendMail, useDb, useLogger, useSafeValidatedBody, useSafeValidatedParams, useSafeValidatedQuery, useValidatedBody, useValidatedParams, useValidatedQuery, useWervtAccess, useWervtUser, useWorkloadIdentity, vh };
|
|
@@ -48,4 +48,5 @@ export declare const TOOL_ROUTE: unique symbol;
|
|
|
48
48
|
export declare function defineToolRoute<R, P extends Input | undefined = undefined, I extends Input | undefined = undefined>(route: ToolRoute<P, I, R>): import("nuxt/server").EventHandler<Promise<R>> & {
|
|
49
49
|
[TOOL_ROUTE]: Tool<Input | undefined, unknown>;
|
|
50
50
|
};
|
|
51
|
-
//#endregion
|
|
51
|
+
//#endregion
|
|
52
|
+
export {};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DEV_LOGIN_COOKIE, WervtUser, parseWervtUser } from "../shared/user.js";
|
|
1
|
+
import { DEV_LOGIN_COOKIE, WervtRole, WervtUser, parseWervtUser } from "../shared/user.js";
|
|
2
2
|
import { RequestEvent } from "nuxt/server";
|
|
3
3
|
//#region src/runtime/server/user.d.ts
|
|
4
4
|
/**
|
|
@@ -7,6 +7,12 @@ import { RequestEvent } from "nuxt/server";
|
|
|
7
7
|
* apps get one when the visitor is signed in. Send people to `/_wervt/login` to sign in.
|
|
8
8
|
* In a tool called over MCP, it's the user the client signed in as.
|
|
9
9
|
*/
|
|
10
|
-
export declare
|
|
10
|
+
export declare function useWervtUser(event: RequestEvent): WervtUser | undefined;
|
|
11
|
+
/**
|
|
12
|
+
* The signed-in user if their role is one of `roles`, e.g.
|
|
13
|
+
* `requireWervtRole(event, 'owner', 'member', 'admin')` for the app's own admin features.
|
|
14
|
+
* Throws 401 without a signed-in user, 403 with another role.
|
|
15
|
+
*/
|
|
16
|
+
export declare function requireWervtRole(event: RequestEvent, ...roles: WervtRole[]): WervtUser;
|
|
11
17
|
//#endregion
|
|
12
|
-
export { DEV_LOGIN_COOKIE, type WervtUser, parseWervtUser };
|
|
18
|
+
export { DEV_LOGIN_COOKIE, type WervtRole, type WervtUser, parseWervtUser };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DEV_LOGIN_COOKIE, devUser, parseWervtUser } from "../shared/user.js";
|
|
2
|
-
import { getCookie } from "nuxt/server";
|
|
2
|
+
import { createError, getCookie } from "nuxt/server";
|
|
3
3
|
//#region src/runtime/server/user.ts
|
|
4
4
|
const MCP_USER = "wervtMcpUser";
|
|
5
5
|
/**
|
|
@@ -8,6 +8,31 @@ const MCP_USER = "wervtMcpUser";
|
|
|
8
8
|
* apps get one when the visitor is signed in. Send people to `/_wervt/login` to sign in.
|
|
9
9
|
* In a tool called over MCP, it's the user the client signed in as.
|
|
10
10
|
*/
|
|
11
|
-
|
|
11
|
+
function useWervtUser(event) {
|
|
12
|
+
const header = parseWervtUser(event.req.headers.get("x-wervt-user"));
|
|
13
|
+
const mcp = event.context[MCP_USER];
|
|
14
|
+
if (mcp) return mcp.role || header?.id !== mcp.id ? mcp : {
|
|
15
|
+
...mcp,
|
|
16
|
+
role: header.role
|
|
17
|
+
};
|
|
18
|
+
return header ?? devUser(getCookie(event, "wervt_dev_login") === "1");
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The signed-in user if their role is one of `roles`, e.g.
|
|
22
|
+
* `requireWervtRole(event, 'owner', 'member', 'admin')` for the app's own admin features.
|
|
23
|
+
* Throws 401 without a signed-in user, 403 with another role.
|
|
24
|
+
*/
|
|
25
|
+
function requireWervtRole(event, ...roles) {
|
|
26
|
+
const user = useWervtUser(event);
|
|
27
|
+
if (!user) throw createError({
|
|
28
|
+
status: 401,
|
|
29
|
+
message: "sign in at /_wervt/login first"
|
|
30
|
+
});
|
|
31
|
+
if (!user.role || !roles.includes(user.role)) throw createError({
|
|
32
|
+
status: 403,
|
|
33
|
+
message: `needs role ${roles.join(", ")}`
|
|
34
|
+
});
|
|
35
|
+
return user;
|
|
36
|
+
}
|
|
12
37
|
//#endregion
|
|
13
|
-
export { DEV_LOGIN_COOKIE, parseWervtUser, useWervtUser };
|
|
38
|
+
export { DEV_LOGIN_COOKIE, parseWervtUser, requireWervtRole, useWervtUser };
|
|
@@ -40,4 +40,5 @@ export declare const vh: {
|
|
|
40
40
|
/** Positive integer id (e.g. a serial primary key) → number. */
|
|
41
41
|
id: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.DigitsAction<string, undefined>, v.TransformAction<any, number>, v.SafeIntegerAction<number, undefined>, v.MinValueAction<number, 1, undefined>]>;
|
|
42
42
|
};
|
|
43
|
-
//#endregion
|
|
43
|
+
//#endregion
|
|
44
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
//#region src/runtime/shared/agent-token.d.ts
|
|
2
|
+
/** Who minted the agent token a request came with, and its id (the control plane's log). */
|
|
3
|
+
export interface AgentToken {
|
|
4
|
+
id: string;
|
|
5
|
+
by: string;
|
|
6
|
+
}
|
|
7
|
+
/** Parses the `x-wervt-agent-token` header the runtime sets (and strips from client requests). */
|
|
8
|
+
export declare function parseAgentToken(header: string | null | undefined): AgentToken | undefined;
|
|
9
|
+
//#endregion
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
//#region src/runtime/shared/agent-token.ts
|
|
2
|
+
/** Parses the `x-wervt-agent-token` header the runtime sets (and strips from client requests). */
|
|
3
|
+
function parseAgentToken(header) {
|
|
4
|
+
if (!header) return void 0;
|
|
5
|
+
try {
|
|
6
|
+
const value = JSON.parse(decodeURIComponent(header));
|
|
7
|
+
if (typeof value?.id !== "string" || typeof value.by !== "string") return void 0;
|
|
8
|
+
return {
|
|
9
|
+
id: value.id,
|
|
10
|
+
by: value.by
|
|
11
|
+
};
|
|
12
|
+
} catch {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
//#endregion
|
|
17
|
+
export { parseAgentToken };
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
//#region src/runtime/shared/user.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* What the user is to this app: its `owner` or one of its `member`s (`wervt members`), a wervt
|
|
4
|
+
* `admin`, someone `invited` through the app (invite-only apps) or `allowed` by the app's list
|
|
5
|
+
* (`wervt access --add`). `public` is anyone else signed in to a public app.
|
|
6
|
+
*/
|
|
7
|
+
export type WervtRole = 'owner' | 'member' | 'admin' | 'invited' | 'allowed' | 'public';
|
|
2
8
|
export interface WervtUser {
|
|
3
9
|
id: string;
|
|
4
10
|
email: string;
|
|
@@ -6,6 +12,8 @@ export interface WervtUser {
|
|
|
6
12
|
image?: string;
|
|
7
13
|
/** Linked login accounts, provider id to the user's id there, e.g. `{ twitch: '1234' }`. */
|
|
8
14
|
accounts?: Record<string, string>;
|
|
15
|
+
/** Missing only for users an app's own `/mcp` tokens stand for (`wervt:mcp:token`). */
|
|
16
|
+
role?: WervtRole;
|
|
9
17
|
}
|
|
10
18
|
/** Cookie set by the dev stand-in for `/_wervt/login` (see `server/dev-auth.ts`). */
|
|
11
19
|
export declare const DEV_LOGIN_COOKIE = "wervt_dev_login";
|
|
@@ -14,7 +14,10 @@ function parseWervtUser(header) {
|
|
|
14
14
|
function devUser(loggedIn) {
|
|
15
15
|
const user = process.env.WERVT_DEV_USER;
|
|
16
16
|
if (!import.meta.dev || !loggedIn || !user) return void 0;
|
|
17
|
-
return
|
|
17
|
+
return {
|
|
18
|
+
role: "owner",
|
|
19
|
+
...JSON.parse(user)
|
|
20
|
+
};
|
|
18
21
|
}
|
|
19
22
|
//#endregion
|
|
20
23
|
export { DEV_LOGIN_COOKIE, devUser, parseWervtUser };
|
package/dist/storage.d.ts
CHANGED
|
@@ -13,14 +13,61 @@ export interface StorageUsage {
|
|
|
13
13
|
bytes: number;
|
|
14
14
|
/** Multipart uploads started but never completed or aborted. */
|
|
15
15
|
unfinishedUploads: number;
|
|
16
|
+
/** The bucket's size limit in bytes, null for none. */
|
|
17
|
+
maxBytes: number | null;
|
|
18
|
+
}
|
|
19
|
+
export interface StorageQuota {
|
|
20
|
+
/** Bytes the bucket may hold; null for no limit. Garage refuses uploads past it. */
|
|
21
|
+
maxBytes: number | null;
|
|
16
22
|
}
|
|
17
23
|
export declare class StorageError extends Error {}
|
|
24
|
+
export interface StoredObject {
|
|
25
|
+
key: string;
|
|
26
|
+
size: number;
|
|
27
|
+
/** ISO time. */
|
|
28
|
+
lastModified: string;
|
|
29
|
+
}
|
|
30
|
+
export interface ObjectListing {
|
|
31
|
+
prefix: string;
|
|
32
|
+
/** Prefixes one level below `prefix`, ending in `/`. */
|
|
33
|
+
folders: string[];
|
|
34
|
+
files: StoredObject[];
|
|
35
|
+
/** Pass as `token` for the next page. */
|
|
36
|
+
next?: string;
|
|
37
|
+
}
|
|
38
|
+
export interface ObjectDetails extends StoredObject {
|
|
39
|
+
contentType: string | null;
|
|
40
|
+
/** `x-amz-meta-*` headers, without the prefix. */
|
|
41
|
+
metadata: Record<string, string>;
|
|
42
|
+
}
|
|
43
|
+
/** S3's limit for object keys, in bytes. */
|
|
44
|
+
export declare const MAX_KEY_BYTES = 1024;
|
|
18
45
|
export declare function createStorage(options: StorageOptions): {
|
|
19
46
|
ensureLayout: () => Promise<void>;
|
|
20
|
-
provision: (app: string, appOrigin: string) => Promise<{
|
|
47
|
+
provision: (app: string, appOrigin: string, quota?: StorageQuota) => Promise<{
|
|
21
48
|
bucket: string;
|
|
22
49
|
}>;
|
|
50
|
+
setQuota: (app: string, quota: StorageQuota) => Promise<boolean>;
|
|
23
51
|
usage: (app: string) => Promise<StorageUsage | undefined>;
|
|
52
|
+
list: (app: string, { prefix, token, limit }?: {
|
|
53
|
+
prefix?: string;
|
|
54
|
+
token?: string;
|
|
55
|
+
limit?: number;
|
|
56
|
+
}) => Promise<ObjectListing | undefined>;
|
|
57
|
+
describe: (app: string, key: string) => Promise<ObjectDetails | undefined>;
|
|
58
|
+
downloadUrl: (app: string, key: string, { publicUrl, expiresIn }: {
|
|
59
|
+
publicUrl: string;
|
|
60
|
+
expiresIn?: number;
|
|
61
|
+
}) => Promise<string>;
|
|
62
|
+
remove: (app: string, key: string) => Promise<{
|
|
63
|
+
deleted: string[];
|
|
64
|
+
} | undefined>;
|
|
65
|
+
sweepAbandonedUploads: (app: string, { graceMs, now }?: {
|
|
66
|
+
graceMs?: number | undefined;
|
|
67
|
+
now?: number | undefined;
|
|
68
|
+
}) => Promise<{
|
|
69
|
+
deleted: string[];
|
|
70
|
+
}>;
|
|
24
71
|
drop: (app: string) => Promise<void>;
|
|
25
72
|
};
|
|
26
73
|
//#endregion
|