@agent-native/dispatch 0.14.2 → 0.14.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 +6 -0
- package/dist/actions/approve-dispatch-change.d.ts +10 -10
- package/dist/actions/provider-api-register.js +64 -2
- package/dist/actions/provider-api-register.js.map +1 -1
- package/dist/actions/reject-dispatch-change.d.ts +10 -10
- package/dist/server/lib/dispatch-store.d.ts +24 -21
- package/dist/server/lib/dispatch-store.d.ts.map +1 -1
- package/dist/server/lib/dispatch-store.js +59 -23
- package/dist/server/lib/dispatch-store.js.map +1 -1
- package/dist/server/lib/vault-store.d.ts.map +1 -1
- package/dist/server/lib/vault-store.js +102 -37
- package/dist/server/lib/vault-store.js.map +1 -1
- package/package.json +2 -3
- package/src/actions/provider-api-register.spec.ts +193 -0
- package/src/actions/provider-api-register.ts +66 -1
- package/src/server/lib/approval-fencing.spec.ts +309 -0
- package/src/server/lib/dispatch-store.ts +92 -21
- package/src/server/lib/vault-store.ts +151 -45
package/README.md
CHANGED
|
@@ -135,6 +135,12 @@ Join the **[Discord](https://discord.gg/qm82StQ2NC)** to ask questions, share wh
|
|
|
135
135
|
|
|
136
136
|
Full documentation at **[agent-native.com](https://agent-native.com)**.
|
|
137
137
|
|
|
138
|
+
## Contributing
|
|
139
|
+
|
|
140
|
+
Working on this repo itself (not just building an app with it)? See
|
|
141
|
+
**[DEVELOPMENT.md](./DEVELOPMENT.md)** for local setup, workspace structure,
|
|
142
|
+
and guard scripts.
|
|
143
|
+
|
|
138
144
|
## License
|
|
139
145
|
|
|
140
146
|
MIT
|
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
declare const _default: import("@agent-native/core").ActionDefinition<{
|
|
2
2
|
id: string;
|
|
3
3
|
}, {
|
|
4
|
+
afterValue: string | null;
|
|
5
|
+
beforeValue: string | null;
|
|
6
|
+
changeType: string;
|
|
7
|
+
createdAt: number;
|
|
4
8
|
id: string;
|
|
5
|
-
ownerEmail: string;
|
|
6
9
|
orgId: string | null;
|
|
7
|
-
|
|
8
|
-
targetType: string;
|
|
9
|
-
targetId: string | null;
|
|
10
|
-
status: string;
|
|
11
|
-
summary: string;
|
|
10
|
+
ownerEmail: string;
|
|
12
11
|
payload: string;
|
|
13
|
-
beforeValue: string | null;
|
|
14
|
-
afterValue: string | null;
|
|
15
12
|
requestedBy: string;
|
|
16
|
-
reviewedBy: string | null;
|
|
17
13
|
reviewedAt: number | null;
|
|
18
|
-
|
|
14
|
+
reviewedBy: string | null;
|
|
15
|
+
status: string;
|
|
16
|
+
summary: string;
|
|
17
|
+
targetId: string | null;
|
|
18
|
+
targetType: string;
|
|
19
19
|
updatedAt: number;
|
|
20
20
|
}>;
|
|
21
21
|
export default _default;
|
|
@@ -1,7 +1,42 @@
|
|
|
1
1
|
import { defineAction } from "@agent-native/core";
|
|
2
|
-
import {
|
|
2
|
+
import { getDbExec } from "@agent-native/core/db";
|
|
3
|
+
import { upsertCustomProvider, deleteCustomProvider, listCustomProviders, getCustomProvider, assertCanMutateCustomProviderScope, } from "@agent-native/core/provider-api";
|
|
3
4
|
import { getCredentialContext } from "@agent-native/core/server";
|
|
4
5
|
import { z } from "zod";
|
|
6
|
+
/**
|
|
7
|
+
* Resolve the caller's role in a specific org, straight from `org_members`.
|
|
8
|
+
*
|
|
9
|
+
* `getCredentialContext()` (from `@agent-native/core/server`) only exposes
|
|
10
|
+
* `{ userEmail, orgId }` — no role. `getOrgContext()` (from
|
|
11
|
+
* `@agent-native/core/org`) resolves role but requires an `H3Event`, which
|
|
12
|
+
* `defineAction` handlers are not given. This mirrors the established
|
|
13
|
+
* no-event role-lookup idiom already used for org-admin gating inside
|
|
14
|
+
* agent-callable/background code (see `isCurrentUserOrgAdmin` in
|
|
15
|
+
* `packages/core/src/jobs/tools.ts`, `getViewerOrgRole` in
|
|
16
|
+
* `packages/dispatch/src/server/lib/usage-metrics-store.ts`, and the same
|
|
17
|
+
* query in `packages/core/src/mcp/actions/service-token-access.ts`) — same
|
|
18
|
+
* SQL, same fail-closed-to-null-on-error semantics. Returns null (never an
|
|
19
|
+
* org role) on any lookup error or when the caller has no membership row in
|
|
20
|
+
* this org.
|
|
21
|
+
*/
|
|
22
|
+
async function resolveCallerOrgRole(orgId, email) {
|
|
23
|
+
if (!orgId)
|
|
24
|
+
return null;
|
|
25
|
+
try {
|
|
26
|
+
const client = getDbExec();
|
|
27
|
+
const { rows } = await client.execute({
|
|
28
|
+
sql: `SELECT role FROM org_members WHERE org_id = ? AND LOWER(email) = ? LIMIT 1`,
|
|
29
|
+
args: [orgId, email.toLowerCase()],
|
|
30
|
+
});
|
|
31
|
+
if (rows.length === 0)
|
|
32
|
+
return null;
|
|
33
|
+
const role = rows[0].role;
|
|
34
|
+
return typeof role === "string" && role ? role : null;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
5
40
|
const AuthSchema = z.discriminatedUnion("type", [
|
|
6
41
|
z.object({
|
|
7
42
|
type: z.literal("none"),
|
|
@@ -98,6 +133,32 @@ After registration the provider appears in provider-api-catalog and can be used
|
|
|
98
133
|
throw new Error("provider-api-register requires an authenticated request context.");
|
|
99
134
|
}
|
|
100
135
|
const scopeId = scope === "org" ? (ctx.orgId ?? ctx.userEmail) : ctx.userEmail;
|
|
136
|
+
// Only upsert/delete mutate state; list/get remain readable by any org
|
|
137
|
+
// member (scoping org-scoped reads is left as a follow-up — see plan
|
|
138
|
+
// 014). Resolve the caller's role in the *target* org (`scopeId`, which
|
|
139
|
+
// for scope === "org" is exactly `ctx.orgId`) and enforce owner/admin
|
|
140
|
+
// before allowing an org-scoped write. `assertCanMutateCustomProviderScope`
|
|
141
|
+
// is the single source of truth for this check and is also enforced a
|
|
142
|
+
// second time inside `upsertCustomProvider`/`deleteCustomProvider`
|
|
143
|
+
// themselves (defense in depth) — calling it here too gives a clear,
|
|
144
|
+
// early error before any other work happens.
|
|
145
|
+
//
|
|
146
|
+
// When the caller has no active org (`ctx.orgId` is null — a solo user,
|
|
147
|
+
// or an app that hasn't wired `resolveOrgId` at all), `scopeId` above
|
|
148
|
+
// already collapsed to `ctx.userEmail`: there is no shared org resource
|
|
149
|
+
// to protect, and no *other* caller can ever address that same scopeId
|
|
150
|
+
// (every other request's fallback is scoped to *its own* email). Treat
|
|
151
|
+
// that case like sole ownership of a personal bucket — consistent with
|
|
152
|
+
// `org/context.ts`'s auto-created personal org, which also assigns the
|
|
153
|
+
// user role "owner" — rather than hard-rejecting scope: "org" (the
|
|
154
|
+
// action's own default) for every solo user or org-less app.
|
|
155
|
+
let orgRole = null;
|
|
156
|
+
if ((operation === "upsert" || operation === "delete") && scope === "org") {
|
|
157
|
+
orgRole = ctx.orgId
|
|
158
|
+
? await resolveCallerOrgRole(ctx.orgId, ctx.userEmail)
|
|
159
|
+
: "owner";
|
|
160
|
+
assertCanMutateCustomProviderScope(scope, scopeId, orgRole);
|
|
161
|
+
}
|
|
101
162
|
if (operation === "list") {
|
|
102
163
|
const providers = await listCustomProviders(scope, scopeId);
|
|
103
164
|
return {
|
|
@@ -125,7 +186,7 @@ After registration the provider appears in provider-api-catalog and can be used
|
|
|
125
186
|
if (operation === "delete") {
|
|
126
187
|
if (!id)
|
|
127
188
|
throw new Error("id is required for delete operation.");
|
|
128
|
-
const deleted = await deleteCustomProvider(scope, scopeId, id);
|
|
189
|
+
const deleted = await deleteCustomProvider(scope, scopeId, id, orgRole);
|
|
129
190
|
return { deleted, id };
|
|
130
191
|
}
|
|
131
192
|
// upsert
|
|
@@ -148,6 +209,7 @@ After registration the provider appears in provider-api-catalog and can be used
|
|
|
148
209
|
allowedHostSuffixes,
|
|
149
210
|
defaultHeaders,
|
|
150
211
|
notes,
|
|
212
|
+
orgRole,
|
|
151
213
|
});
|
|
152
214
|
return {
|
|
153
215
|
registered: true,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"provider-api-register.js","sourceRoot":"","sources":["../../src/actions/provider-api-register.ts"],"names":[],"mappings":"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","sourcesContent":["import { defineAction } from \"@agent-native/core\";\nimport {\n upsertCustomProvider,\n deleteCustomProvider,\n listCustomProviders,\n getCustomProvider,\n} from \"@agent-native/core/provider-api\";\nimport { getCredentialContext } from \"@agent-native/core/server\";\nimport { z } from \"zod\";\n\nconst AuthSchema = z.discriminatedUnion(\"type\", [\n z.object({\n type: z.literal(\"none\"),\n }),\n z.object({\n type: z.literal(\"bearer\"),\n credentialKey: z\n .string()\n .min(1)\n .describe(\n \"Name of the credential key (e.g. MY_API_TOKEN). Must already be saved via app secrets — this action never accepts secret values.\",\n ),\n }),\n z.object({\n type: z.literal(\"basic\"),\n usernameKey: z\n .string()\n .min(1)\n .describe(\"Credential key name for the username/login.\"),\n passwordKey: z\n .string()\n .min(1)\n .describe(\"Credential key name for the password/secret.\"),\n }),\n z.object({\n type: z.literal(\"api-key-header\"),\n credentialKey: z\n .string()\n .min(1)\n .describe(\"Credential key name for the API key value.\"),\n headerName: z\n .string()\n .min(1)\n .describe(\"HTTP header name to send the key in (e.g. X-Api-Key).\"),\n }),\n]);\n\nexport default defineAction({\n description: `Register or update a custom API provider so the agent can call it via provider-api-request and look up its docs via provider-api-docs.\n\nIMPORTANT — credentials:\n- This action stores only credential KEY NAMES, never secret values.\n- If the required API key is not yet saved, instruct the user to add it via app Settings → Keys, or use the create-vault-secret action, before calling this action.\n- Supported auth kinds: none, bearer, basic, api-key-header.\n google-service-account and oauth-bearer are NOT supported for custom providers.\n\nAfter registration the provider appears in provider-api-catalog and can be used with provider-api-request.`,\n schema: z.object({\n operation: z\n .enum([\"upsert\", \"delete\", \"list\", \"get\"])\n .default(\"upsert\")\n .describe(\n \"Operation: upsert (create or update), delete (remove), list (all custom providers), get (single provider).\",\n ),\n id: z\n .string()\n .min(1)\n .max(64)\n .optional()\n .describe(\n \"Provider slug (e.g. my-api). Lowercase letters, digits, hyphens only. Required for upsert/delete/get.\",\n ),\n label: z\n .string()\n .min(1)\n .max(200)\n .optional()\n .describe(\n \"Human-readable name (e.g. 'My Analytics API'). Required for upsert.\",\n ),\n baseUrl: z\n .string()\n .url()\n .optional()\n .describe(\n \"Base URL for the API (e.g. https://api.example.com/v1). Required for upsert. Must be a public https/http URL.\",\n ),\n auth: AuthSchema.optional().describe(\n \"Auth configuration. Required for upsert. Use type 'none' for public APIs.\",\n ),\n docsUrls: z\n .array(z.string().url())\n .optional()\n .describe(\"Optional list of documentation URLs for this provider.\"),\n allowedHostSuffixes: z\n .array(z.string())\n .optional()\n .describe(\n \"Optional list of additional host suffixes requests may target beyond the base URL origin (e.g. ['example.com']).\",\n ),\n defaultHeaders: z\n .record(z.string(), z.string())\n .optional()\n .describe(\n \"Optional headers to include on every request (e.g. { 'Accept': 'application/json' }).\",\n ),\n notes: z\n .string()\n .max(1000)\n .optional()\n .describe(\"Optional notes about this provider shown in the catalog.\"),\n scope: z\n .enum([\"user\", \"org\"])\n .default(\"org\")\n .describe(\n \"Whether to store the provider for the current user only ('user') or for the whole workspace ('org').\",\n ),\n }),\n http: false,\n run: async ({\n operation,\n id,\n label,\n baseUrl,\n auth,\n docsUrls,\n allowedHostSuffixes,\n defaultHeaders,\n notes,\n scope,\n }) => {\n const ctx = getCredentialContext();\n if (!ctx) {\n throw new Error(\n \"provider-api-register requires an authenticated request context.\",\n );\n }\n const scopeId =\n scope === \"org\" ? (ctx.orgId ?? ctx.userEmail) : ctx.userEmail;\n\n if (operation === \"list\") {\n const providers = await listCustomProviders(scope, scopeId);\n return {\n providers: providers.map((p) => ({\n id: p.id,\n label: p.label,\n baseUrl: p.baseUrl,\n authType: p.auth.type,\n docsUrls: p.docsUrls,\n notes: p.notes,\n updatedAt: p.updatedAt,\n })),\n count: providers.length,\n };\n }\n\n if (operation === \"get\") {\n if (!id) throw new Error(\"id is required for get operation.\");\n const provider = await getCustomProvider(scope, scopeId, id);\n if (!provider) {\n return { found: false, id };\n }\n return { found: true, provider };\n }\n\n if (operation === \"delete\") {\n if (!id) throw new Error(\"id is required for delete operation.\");\n const deleted = await deleteCustomProvider(scope, scopeId, id);\n return { deleted, id };\n }\n\n // upsert\n if (!id) throw new Error(\"id is required for upsert operation.\");\n if (!label) throw new Error(\"label is required for upsert operation.\");\n if (!baseUrl) throw new Error(\"baseUrl is required for upsert operation.\");\n if (!auth) throw new Error(\"auth is required for upsert operation.\");\n\n await upsertCustomProvider({\n scope,\n scopeId,\n id,\n label,\n baseUrl,\n auth,\n docsUrls,\n allowedHostSuffixes,\n defaultHeaders,\n notes,\n });\n\n return {\n registered: true,\n id,\n label,\n message: `Custom provider \"${id}\" registered. Use provider-api-catalog to inspect it and provider-api-request to call it.`,\n };\n },\n});\n"]}
|
|
1
|
+
{"version":3,"file":"provider-api-register.js","sourceRoot":"","sources":["../../src/actions/provider-api-register.ts"],"names":[],"mappings":"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","sourcesContent":["import { defineAction } from \"@agent-native/core\";\nimport { getDbExec } from \"@agent-native/core/db\";\nimport {\n upsertCustomProvider,\n deleteCustomProvider,\n listCustomProviders,\n getCustomProvider,\n assertCanMutateCustomProviderScope,\n} from \"@agent-native/core/provider-api\";\nimport { getCredentialContext } from \"@agent-native/core/server\";\nimport { z } from \"zod\";\n\n/**\n * Resolve the caller's role in a specific org, straight from `org_members`.\n *\n * `getCredentialContext()` (from `@agent-native/core/server`) only exposes\n * `{ userEmail, orgId }` — no role. `getOrgContext()` (from\n * `@agent-native/core/org`) resolves role but requires an `H3Event`, which\n * `defineAction` handlers are not given. This mirrors the established\n * no-event role-lookup idiom already used for org-admin gating inside\n * agent-callable/background code (see `isCurrentUserOrgAdmin` in\n * `packages/core/src/jobs/tools.ts`, `getViewerOrgRole` in\n * `packages/dispatch/src/server/lib/usage-metrics-store.ts`, and the same\n * query in `packages/core/src/mcp/actions/service-token-access.ts`) — same\n * SQL, same fail-closed-to-null-on-error semantics. Returns null (never an\n * org role) on any lookup error or when the caller has no membership row in\n * this org.\n */\nasync function resolveCallerOrgRole(\n orgId: string | null,\n email: string,\n): Promise<string | null> {\n if (!orgId) return null;\n try {\n const client = getDbExec();\n const { rows } = await client.execute({\n sql: `SELECT role FROM org_members WHERE org_id = ? AND LOWER(email) = ? LIMIT 1`,\n args: [orgId, email.toLowerCase()],\n });\n if (rows.length === 0) return null;\n const role = (rows[0] as { role?: unknown }).role;\n return typeof role === \"string\" && role ? role : null;\n } catch {\n return null;\n }\n}\n\nconst AuthSchema = z.discriminatedUnion(\"type\", [\n z.object({\n type: z.literal(\"none\"),\n }),\n z.object({\n type: z.literal(\"bearer\"),\n credentialKey: z\n .string()\n .min(1)\n .describe(\n \"Name of the credential key (e.g. MY_API_TOKEN). Must already be saved via app secrets — this action never accepts secret values.\",\n ),\n }),\n z.object({\n type: z.literal(\"basic\"),\n usernameKey: z\n .string()\n .min(1)\n .describe(\"Credential key name for the username/login.\"),\n passwordKey: z\n .string()\n .min(1)\n .describe(\"Credential key name for the password/secret.\"),\n }),\n z.object({\n type: z.literal(\"api-key-header\"),\n credentialKey: z\n .string()\n .min(1)\n .describe(\"Credential key name for the API key value.\"),\n headerName: z\n .string()\n .min(1)\n .describe(\"HTTP header name to send the key in (e.g. X-Api-Key).\"),\n }),\n]);\n\nexport default defineAction({\n description: `Register or update a custom API provider so the agent can call it via provider-api-request and look up its docs via provider-api-docs.\n\nIMPORTANT — credentials:\n- This action stores only credential KEY NAMES, never secret values.\n- If the required API key is not yet saved, instruct the user to add it via app Settings → Keys, or use the create-vault-secret action, before calling this action.\n- Supported auth kinds: none, bearer, basic, api-key-header.\n google-service-account and oauth-bearer are NOT supported for custom providers.\n\nAfter registration the provider appears in provider-api-catalog and can be used with provider-api-request.`,\n schema: z.object({\n operation: z\n .enum([\"upsert\", \"delete\", \"list\", \"get\"])\n .default(\"upsert\")\n .describe(\n \"Operation: upsert (create or update), delete (remove), list (all custom providers), get (single provider).\",\n ),\n id: z\n .string()\n .min(1)\n .max(64)\n .optional()\n .describe(\n \"Provider slug (e.g. my-api). Lowercase letters, digits, hyphens only. Required for upsert/delete/get.\",\n ),\n label: z\n .string()\n .min(1)\n .max(200)\n .optional()\n .describe(\n \"Human-readable name (e.g. 'My Analytics API'). Required for upsert.\",\n ),\n baseUrl: z\n .string()\n .url()\n .optional()\n .describe(\n \"Base URL for the API (e.g. https://api.example.com/v1). Required for upsert. Must be a public https/http URL.\",\n ),\n auth: AuthSchema.optional().describe(\n \"Auth configuration. Required for upsert. Use type 'none' for public APIs.\",\n ),\n docsUrls: z\n .array(z.string().url())\n .optional()\n .describe(\"Optional list of documentation URLs for this provider.\"),\n allowedHostSuffixes: z\n .array(z.string())\n .optional()\n .describe(\n \"Optional list of additional host suffixes requests may target beyond the base URL origin (e.g. ['example.com']).\",\n ),\n defaultHeaders: z\n .record(z.string(), z.string())\n .optional()\n .describe(\n \"Optional headers to include on every request (e.g. { 'Accept': 'application/json' }).\",\n ),\n notes: z\n .string()\n .max(1000)\n .optional()\n .describe(\"Optional notes about this provider shown in the catalog.\"),\n scope: z\n .enum([\"user\", \"org\"])\n .default(\"org\")\n .describe(\n \"Whether to store the provider for the current user only ('user') or for the whole workspace ('org').\",\n ),\n }),\n http: false,\n run: async ({\n operation,\n id,\n label,\n baseUrl,\n auth,\n docsUrls,\n allowedHostSuffixes,\n defaultHeaders,\n notes,\n scope,\n }) => {\n const ctx = getCredentialContext();\n if (!ctx) {\n throw new Error(\n \"provider-api-register requires an authenticated request context.\",\n );\n }\n const scopeId =\n scope === \"org\" ? (ctx.orgId ?? ctx.userEmail) : ctx.userEmail;\n\n // Only upsert/delete mutate state; list/get remain readable by any org\n // member (scoping org-scoped reads is left as a follow-up — see plan\n // 014). Resolve the caller's role in the *target* org (`scopeId`, which\n // for scope === \"org\" is exactly `ctx.orgId`) and enforce owner/admin\n // before allowing an org-scoped write. `assertCanMutateCustomProviderScope`\n // is the single source of truth for this check and is also enforced a\n // second time inside `upsertCustomProvider`/`deleteCustomProvider`\n // themselves (defense in depth) — calling it here too gives a clear,\n // early error before any other work happens.\n //\n // When the caller has no active org (`ctx.orgId` is null — a solo user,\n // or an app that hasn't wired `resolveOrgId` at all), `scopeId` above\n // already collapsed to `ctx.userEmail`: there is no shared org resource\n // to protect, and no *other* caller can ever address that same scopeId\n // (every other request's fallback is scoped to *its own* email). Treat\n // that case like sole ownership of a personal bucket — consistent with\n // `org/context.ts`'s auto-created personal org, which also assigns the\n // user role \"owner\" — rather than hard-rejecting scope: \"org\" (the\n // action's own default) for every solo user or org-less app.\n let orgRole: string | null = null;\n if ((operation === \"upsert\" || operation === \"delete\") && scope === \"org\") {\n orgRole = ctx.orgId\n ? await resolveCallerOrgRole(ctx.orgId, ctx.userEmail)\n : \"owner\";\n assertCanMutateCustomProviderScope(scope, scopeId, orgRole);\n }\n\n if (operation === \"list\") {\n const providers = await listCustomProviders(scope, scopeId);\n return {\n providers: providers.map((p) => ({\n id: p.id,\n label: p.label,\n baseUrl: p.baseUrl,\n authType: p.auth.type,\n docsUrls: p.docsUrls,\n notes: p.notes,\n updatedAt: p.updatedAt,\n })),\n count: providers.length,\n };\n }\n\n if (operation === \"get\") {\n if (!id) throw new Error(\"id is required for get operation.\");\n const provider = await getCustomProvider(scope, scopeId, id);\n if (!provider) {\n return { found: false, id };\n }\n return { found: true, provider };\n }\n\n if (operation === \"delete\") {\n if (!id) throw new Error(\"id is required for delete operation.\");\n const deleted = await deleteCustomProvider(scope, scopeId, id, orgRole);\n return { deleted, id };\n }\n\n // upsert\n if (!id) throw new Error(\"id is required for upsert operation.\");\n if (!label) throw new Error(\"label is required for upsert operation.\");\n if (!baseUrl) throw new Error(\"baseUrl is required for upsert operation.\");\n if (!auth) throw new Error(\"auth is required for upsert operation.\");\n\n await upsertCustomProvider({\n scope,\n scopeId,\n id,\n label,\n baseUrl,\n auth,\n docsUrls,\n allowedHostSuffixes,\n defaultHeaders,\n notes,\n orgRole,\n });\n\n return {\n registered: true,\n id,\n label,\n message: `Custom provider \"${id}\" registered. Use provider-api-catalog to inspect it and provider-api-request to call it.`,\n };\n },\n});\n"]}
|
|
@@ -2,21 +2,21 @@ declare const _default: import("@agent-native/core").ActionDefinition<{
|
|
|
2
2
|
id: string;
|
|
3
3
|
reason?: string | undefined;
|
|
4
4
|
}, {
|
|
5
|
+
afterValue: string | null;
|
|
6
|
+
beforeValue: string | null;
|
|
7
|
+
changeType: string;
|
|
8
|
+
createdAt: number;
|
|
5
9
|
id: string;
|
|
6
|
-
ownerEmail: string;
|
|
7
10
|
orgId: string | null;
|
|
8
|
-
|
|
9
|
-
targetType: string;
|
|
10
|
-
targetId: string | null;
|
|
11
|
-
status: string;
|
|
12
|
-
summary: string;
|
|
11
|
+
ownerEmail: string;
|
|
13
12
|
payload: string;
|
|
14
|
-
beforeValue: string | null;
|
|
15
|
-
afterValue: string | null;
|
|
16
13
|
requestedBy: string;
|
|
17
|
-
reviewedBy: string | null;
|
|
18
14
|
reviewedAt: number | null;
|
|
19
|
-
|
|
15
|
+
reviewedBy: string | null;
|
|
16
|
+
status: string;
|
|
17
|
+
summary: string;
|
|
18
|
+
targetId: string | null;
|
|
19
|
+
targetType: string;
|
|
20
20
|
updatedAt: number;
|
|
21
21
|
}>;
|
|
22
22
|
export default _default;
|
|
@@ -35,7 +35,7 @@ export interface DispatchCtx {
|
|
|
35
35
|
}
|
|
36
36
|
export declare function requireDispatchCtx(): DispatchCtx;
|
|
37
37
|
export declare function getApprovalPolicy(): Promise<DispatchApprovalPolicy>;
|
|
38
|
-
export declare function setApprovalPolicy(input: DispatchApprovalPolicy): Promise<
|
|
38
|
+
export declare function setApprovalPolicy(input: DispatchApprovalPolicy): Promise<{
|
|
39
39
|
id: string;
|
|
40
40
|
ownerEmail: string;
|
|
41
41
|
orgId: string | null;
|
|
@@ -52,6 +52,9 @@ export declare function setApprovalPolicy(input: DispatchApprovalPolicy): Promis
|
|
|
52
52
|
reviewedAt: number | null;
|
|
53
53
|
createdAt: number;
|
|
54
54
|
updatedAt: number;
|
|
55
|
+
} | {
|
|
56
|
+
enabled: boolean;
|
|
57
|
+
approverEmails: string[];
|
|
55
58
|
}>;
|
|
56
59
|
export declare function recordAudit(input: {
|
|
57
60
|
action: string;
|
|
@@ -206,39 +209,39 @@ export declare function listApprovalRequests(): Promise<{
|
|
|
206
209
|
updatedAt: number;
|
|
207
210
|
}[]>;
|
|
208
211
|
export declare function approveRequest(requestId: string): Promise<{
|
|
212
|
+
afterValue: string | null;
|
|
213
|
+
beforeValue: string | null;
|
|
214
|
+
changeType: string;
|
|
215
|
+
createdAt: number;
|
|
209
216
|
id: string;
|
|
210
|
-
ownerEmail: string;
|
|
211
217
|
orgId: string | null;
|
|
212
|
-
|
|
213
|
-
targetType: string;
|
|
214
|
-
targetId: string | null;
|
|
215
|
-
status: string;
|
|
216
|
-
summary: string;
|
|
218
|
+
ownerEmail: string;
|
|
217
219
|
payload: string;
|
|
218
|
-
beforeValue: string | null;
|
|
219
|
-
afterValue: string | null;
|
|
220
220
|
requestedBy: string;
|
|
221
|
-
reviewedBy: string | null;
|
|
222
221
|
reviewedAt: number | null;
|
|
223
|
-
|
|
222
|
+
reviewedBy: string | null;
|
|
223
|
+
status: string;
|
|
224
|
+
summary: string;
|
|
225
|
+
targetId: string | null;
|
|
226
|
+
targetType: string;
|
|
224
227
|
updatedAt: number;
|
|
225
228
|
}>;
|
|
226
229
|
export declare function rejectRequest(requestId: string, reason?: string | null): Promise<{
|
|
230
|
+
afterValue: string | null;
|
|
231
|
+
beforeValue: string | null;
|
|
232
|
+
changeType: string;
|
|
233
|
+
createdAt: number;
|
|
227
234
|
id: string;
|
|
228
|
-
ownerEmail: string;
|
|
229
235
|
orgId: string | null;
|
|
230
|
-
|
|
231
|
-
targetType: string;
|
|
232
|
-
targetId: string | null;
|
|
233
|
-
status: string;
|
|
234
|
-
summary: string;
|
|
236
|
+
ownerEmail: string;
|
|
235
237
|
payload: string;
|
|
236
|
-
beforeValue: string | null;
|
|
237
|
-
afterValue: string | null;
|
|
238
238
|
requestedBy: string;
|
|
239
|
-
reviewedBy: string | null;
|
|
240
239
|
reviewedAt: number | null;
|
|
241
|
-
|
|
240
|
+
reviewedBy: string | null;
|
|
241
|
+
status: string;
|
|
242
|
+
summary: string;
|
|
243
|
+
targetId: string | null;
|
|
244
|
+
targetType: string;
|
|
242
245
|
updatedAt: number;
|
|
243
246
|
}>;
|
|
244
247
|
export declare function createLinkToken(platform: string): Promise<{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dispatch-store.d.ts","sourceRoot":"","sources":["../../../src/server/lib/dispatch-store.ts"],"names":[],"mappings":"AAWA,eAAO,MAAM,qBAAqB,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"dispatch-store.d.ts","sourceRoot":"","sources":["../../../src/server/lib/dispatch-store.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
|
-
import { and, desc, eq, isNull, or } from "@agent-native/core/db/schema";
|
|
2
|
+
import { and, desc, eq, isNull, or, sql } from "@agent-native/core/db/schema";
|
|
3
3
|
import { getRequestUserEmail, getRequestOrgId, } from "@agent-native/core/server";
|
|
4
4
|
import { getOrgSetting, putOrgSetting } from "@agent-native/core/settings";
|
|
5
5
|
import { getDb, schema } from "../../db/index.js";
|
|
6
6
|
export const SHARED_DISPATCH_OWNER = "dispatch@shared";
|
|
7
7
|
const APPROVAL_POLICY_KEY = "dispatch-approval-policy";
|
|
8
|
+
const APPROVAL_APPLY_LEASE_MS = 5 * 60 * 1000;
|
|
8
9
|
// ─── /link rate limiting ──────────────────────────────────────────────────
|
|
9
10
|
//
|
|
10
11
|
// Failed `/link <token>` attempts are rate-limited per `(platform, externalUserId)`
|
|
@@ -116,8 +117,7 @@ export async function getApprovalPolicy() {
|
|
|
116
117
|
: [],
|
|
117
118
|
};
|
|
118
119
|
}
|
|
119
|
-
async function applyApprovalPolicy(input, actor = currentOwnerEmail()) {
|
|
120
|
-
const orgId = currentOrgId();
|
|
120
|
+
async function applyApprovalPolicy(input, actor = currentOwnerEmail(), orgId = currentOrgId()) {
|
|
121
121
|
if (!orgId) {
|
|
122
122
|
throw new Error("Dispatch approval settings require an active organization");
|
|
123
123
|
}
|
|
@@ -135,7 +135,10 @@ async function applyApprovalPolicy(input, actor = currentOwnerEmail()) {
|
|
|
135
135
|
metadata: input,
|
|
136
136
|
actor,
|
|
137
137
|
});
|
|
138
|
-
return
|
|
138
|
+
return {
|
|
139
|
+
enabled: input.enabled,
|
|
140
|
+
approverEmails: input.approverEmails,
|
|
141
|
+
};
|
|
139
142
|
}
|
|
140
143
|
export async function setApprovalPolicy(input) {
|
|
141
144
|
const current = await getApprovalPolicy();
|
|
@@ -395,7 +398,7 @@ async function applyApprovedRequest(request) {
|
|
|
395
398
|
return applyDestinationDelete(payload.id, request.reviewedBy || currentOwnerEmail(), requestCtx);
|
|
396
399
|
}
|
|
397
400
|
if (request.changeType === "approval-policy.update") {
|
|
398
|
-
return applyApprovalPolicy(payload, request.reviewedBy || currentOwnerEmail());
|
|
401
|
+
return applyApprovalPolicy(payload, request.reviewedBy || currentOwnerEmail(), requestCtx.orgId);
|
|
399
402
|
}
|
|
400
403
|
if (request.changeType === "dream-proposal.apply") {
|
|
401
404
|
const { applyApprovedDreamProposal } = await import("./dreams-store.js");
|
|
@@ -421,23 +424,53 @@ export async function approveRequest(requestId) {
|
|
|
421
424
|
const request = await getApprovalRequest(requestId, ctx);
|
|
422
425
|
if (!request)
|
|
423
426
|
throw new Error("Approval request not found");
|
|
424
|
-
if (request.status !== "pending") {
|
|
425
|
-
throw new Error("Only pending approvals can be approved");
|
|
426
|
-
}
|
|
427
427
|
const timestamp = now();
|
|
428
|
-
|
|
428
|
+
const staleApplyingBefore = timestamp - APPROVAL_APPLY_LEASE_MS;
|
|
429
|
+
// Fence the transition on the current status so a concurrent approve can't
|
|
430
|
+
// both win. A crashed worker can leave its lease in "applying", so reclaim
|
|
431
|
+
// only a lease that has been idle for the full lease interval.
|
|
432
|
+
const claimed = await db
|
|
433
|
+
.update(schema.dispatchApprovalRequests)
|
|
434
|
+
.set({
|
|
435
|
+
status: "applying",
|
|
436
|
+
updatedAt: timestamp,
|
|
437
|
+
})
|
|
438
|
+
.where(and(eq(schema.dispatchApprovalRequests.id, requestId), ctxScope(schema.dispatchApprovalRequests, ctx), or(eq(schema.dispatchApprovalRequests.status, "pending"), and(eq(schema.dispatchApprovalRequests.status, "applying"), sql `${schema.dispatchApprovalRequests.updatedAt} < ${staleApplyingBefore}`))))
|
|
439
|
+
.returning();
|
|
440
|
+
if (claimed.length === 0) {
|
|
441
|
+
const current = (await getApprovalRequest(requestId, ctx)) ?? request;
|
|
442
|
+
if (current.status === "applying") {
|
|
443
|
+
throw new Error("Approval is already being applied");
|
|
444
|
+
}
|
|
445
|
+
return current;
|
|
446
|
+
}
|
|
447
|
+
const applying = claimed[0];
|
|
448
|
+
try {
|
|
449
|
+
await applyApprovedRequest(applying);
|
|
450
|
+
}
|
|
451
|
+
catch (error) {
|
|
452
|
+
// Applying a request can have succeeded partially before throwing. Do not
|
|
453
|
+
// return it to pending: that would allow an immediate retry to duplicate a
|
|
454
|
+
// non-idempotent effect. Keep the lease until it becomes stale, which
|
|
455
|
+
// serializes any recovery attempt behind the same fencing rule.
|
|
456
|
+
await db
|
|
457
|
+
.update(schema.dispatchApprovalRequests)
|
|
458
|
+
.set({ updatedAt: now() })
|
|
459
|
+
.where(and(eq(schema.dispatchApprovalRequests.id, requestId), ctxScope(schema.dispatchApprovalRequests, ctx), eq(schema.dispatchApprovalRequests.status, "applying")));
|
|
460
|
+
throw error;
|
|
461
|
+
}
|
|
462
|
+
const [updated] = await db
|
|
429
463
|
.update(schema.dispatchApprovalRequests)
|
|
430
464
|
.set({
|
|
431
465
|
status: "approved",
|
|
432
466
|
reviewedBy: currentOwnerEmail(),
|
|
433
467
|
reviewedAt: timestamp,
|
|
434
|
-
updatedAt:
|
|
468
|
+
updatedAt: now(),
|
|
435
469
|
})
|
|
436
|
-
.where(eq(schema.dispatchApprovalRequests.id, requestId))
|
|
437
|
-
|
|
470
|
+
.where(and(eq(schema.dispatchApprovalRequests.id, requestId), eq(schema.dispatchApprovalRequests.status, "applying")))
|
|
471
|
+
.returning();
|
|
438
472
|
if (!updated)
|
|
439
473
|
throw new Error("Approval request disappeared");
|
|
440
|
-
await applyApprovedRequest(updated);
|
|
441
474
|
await recordAudit({
|
|
442
475
|
action: "approval.approved",
|
|
443
476
|
targetType: updated.targetType,
|
|
@@ -449,14 +482,12 @@ export async function approveRequest(requestId) {
|
|
|
449
482
|
}
|
|
450
483
|
export async function rejectRequest(requestId, reason) {
|
|
451
484
|
const db = getDb();
|
|
452
|
-
const
|
|
485
|
+
const ctx = requireDispatchCtx();
|
|
486
|
+
const request = await getApprovalRequest(requestId, ctx);
|
|
453
487
|
if (!request)
|
|
454
488
|
throw new Error("Approval request not found");
|
|
455
|
-
if (request.status !== "pending") {
|
|
456
|
-
throw new Error("Only pending approvals can be rejected");
|
|
457
|
-
}
|
|
458
489
|
const timestamp = now();
|
|
459
|
-
await db
|
|
490
|
+
const claimed = await db
|
|
460
491
|
.update(schema.dispatchApprovalRequests)
|
|
461
492
|
.set({
|
|
462
493
|
status: "rejected",
|
|
@@ -464,15 +495,20 @@ export async function rejectRequest(requestId, reason) {
|
|
|
464
495
|
reviewedAt: timestamp,
|
|
465
496
|
updatedAt: timestamp,
|
|
466
497
|
})
|
|
467
|
-
.where(eq(schema.dispatchApprovalRequests.id, requestId))
|
|
498
|
+
.where(and(eq(schema.dispatchApprovalRequests.id, requestId), eq(schema.dispatchApprovalRequests.status, "pending")))
|
|
499
|
+
.returning();
|
|
500
|
+
if (claimed.length === 0) {
|
|
501
|
+
return (await getApprovalRequest(requestId, ctx)) ?? request;
|
|
502
|
+
}
|
|
503
|
+
const updated = claimed[0];
|
|
468
504
|
await recordAudit({
|
|
469
505
|
action: "approval.rejected",
|
|
470
|
-
targetType:
|
|
506
|
+
targetType: updated.targetType,
|
|
471
507
|
targetId: requestId,
|
|
472
|
-
summary: `Rejected ${
|
|
473
|
-
metadata: { request, reason: reason || null },
|
|
508
|
+
summary: `Rejected ${updated.summary}`,
|
|
509
|
+
metadata: { request: updated, reason: reason || null },
|
|
474
510
|
});
|
|
475
|
-
return
|
|
511
|
+
return updated;
|
|
476
512
|
}
|
|
477
513
|
export async function createLinkToken(platform) {
|
|
478
514
|
const db = getDb();
|