create-ailk 0.14.0 → 0.16.0
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 +124 -0
- package/component-catalog.json +3 -3
- package/dist/add-module.d.ts +7 -0
- package/dist/add-module.js +80 -3
- package/dist/add-route.js +26 -2
- package/dist/adopt.d.ts +74 -0
- package/dist/adopt.js +314 -0
- package/dist/apply-design.d.ts +12 -1
- package/dist/apply-design.js +36 -4
- package/dist/cli.js +103 -3
- package/dist/derive-architecture.d.ts +37 -2
- package/dist/derive-architecture.js +92 -7
- package/dist/design-package.js +3 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/lib/chrome-readers.d.ts +21 -0
- package/dist/lib/chrome-readers.js +100 -0
- package/dist/lib/declared-env.d.ts +142 -0
- package/dist/lib/declared-env.js +985 -0
- package/dist/lib/env-catalog.d.ts +17 -0
- package/dist/lib/env-catalog.js +234 -0
- package/dist/lib/extract-module-bundle.d.ts +20 -0
- package/dist/lib/extract-module-bundle.js +53 -6
- package/dist/lib/license-key-files.d.ts +6 -1
- package/dist/lib/license-key-files.js +9 -4
- package/dist/lib/module-tier.d.ts +2 -0
- package/dist/lib/module-tier.js +4 -0
- package/dist/module-architecture.d.ts +9 -0
- package/dist/module-architecture.js +82 -0
- package/dist/parse-args.d.ts +27 -0
- package/dist/parse-args.js +89 -0
- package/dist/programmatic.d.ts +28 -0
- package/dist/programmatic.js +181 -11
- package/dist/seed-flows.d.ts +65 -0
- package/dist/seed-flows.js +350 -0
- package/dist/site-chrome.d.ts +4 -0
- package/dist/site-chrome.js +34 -3
- package/dist/site-manifest.d.ts +55 -5
- package/dist/site-manifest.js +121 -12
- package/dist/site-roster.d.ts +13 -0
- package/dist/site-roster.js +22 -8
- package/dist/spoke-checkout.d.ts +42 -0
- package/dist/spoke-checkout.js +359 -0
- package/dist/surfaces.js +7 -0
- package/dist/sync-routes.js +29 -5
- package/dist/topology.d.ts +246 -0
- package/dist/topology.js +942 -0
- package/package.json +3 -3
- package/site-templates/designs/minimalism/DESIGN.md +3 -3
- package/templates/.env.example +10 -0
- package/templates/apps/api/CLAUDE.md +21 -4
- package/templates/apps/api/package.json +1 -0
- package/templates/apps/api/src/__tests__/factories/agent-caller.ts +5 -1
- package/templates/apps/api/src/__tests__/factories/in-memory-prisma.ts +95 -16
- package/templates/apps/api/src/__tests__/openapi-keyless-hub.test.ts +122 -0
- package/templates/apps/api/src/__tests__/support/hub-sign-in-fake-db.ts +362 -0
- package/templates/apps/api/src/bin/spokes.ts +62 -1
- package/templates/apps/api/src/config/__tests__/hub-require-spoke-key.test.ts +75 -0
- package/templates/apps/api/src/config/index.ts +35 -0
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +99 -0
- package/templates/apps/api/src/lib/__tests__/agent-store.test.ts +114 -1
- package/templates/apps/api/src/lib/__tests__/deliverable-asset-store.test.ts +66 -5
- package/templates/apps/api/src/lib/__tests__/hub-sign-in-store.test.ts +209 -0
- package/templates/apps/api/src/lib/__tests__/site.test.ts +54 -0
- package/templates/apps/api/src/lib/__tests__/spoke-store.test.ts +20 -0
- package/templates/apps/api/src/lib/agent-store.ts +95 -3
- package/templates/apps/api/src/lib/deliverable-asset-store.ts +24 -5
- package/templates/apps/api/src/lib/hub-sign-in-store.ts +419 -0
- package/templates/apps/api/src/lib/site.ts +37 -0
- package/templates/apps/api/src/lib/spoke-store.ts +41 -3
- package/templates/apps/api/src/middleware/__tests__/auth.bearer-path.test.ts +4 -0
- package/templates/apps/api/src/middleware/__tests__/auth.hub-session-path.test.ts +310 -0
- package/templates/apps/api/src/middleware/__tests__/default-tenant.test.ts +22 -0
- package/templates/apps/api/src/middleware/auth.ts +129 -0
- package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +23 -0
- package/templates/apps/api/src/openapi/spec.ts +38 -2
- package/templates/apps/api/src/routes/auth/__tests__/hub-sign-in.test.ts +483 -0
- package/templates/apps/api/src/routes/auth/hub-sign-in.ts +201 -0
- package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +50 -2
- package/templates/apps/api/src/routes/checkout/sessions.ts +7 -0
- package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +282 -0
- package/templates/apps/api/src/routes/leads/index.ts +69 -6
- package/templates/apps/api/src/routes/webhooks/README.md +39 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +105 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-site-filter.test.ts +366 -0
- package/templates/apps/api/src/routes/webhooks/stripe.ts +133 -2
- package/templates/apps/api/src/server.ts +105 -0
- package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +4 -4
- package/templates/apps/api/src/services/__tests__/hub-sign-in.test.ts +854 -0
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +492 -0
- package/templates/apps/api/src/services/__tests__/person.test.ts +553 -0
- package/templates/apps/api/src/services/default-tenant.ts +41 -7
- package/templates/apps/api/src/services/hub-sign-in.ts +471 -0
- package/templates/apps/api/src/services/lead-routing.ts +278 -30
- package/templates/apps/api/src/services/person.ts +541 -0
- package/templates/apps/api/src/services/public-site-resolver.ts +48 -0
- package/templates/apps/mcp/__tests__/index.test.ts +5 -5
- package/templates/apps/mcp/src/index.ts +6 -4
- package/templates/apps/mcp/src/lib/content-adapter.ts +2 -2
- package/templates/apps/mcp/src/lib/topology.generated.ts +24 -0
- package/templates/apps/web/.env.example +58 -5
- package/templates/apps/web/CLAUDE.md +1 -0
- package/templates/apps/web/app/[locale]/(auth)/sign-in/next-path.ts +0 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/hub-mode.test.tsx +206 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +256 -3
- package/templates/apps/web/app/[locale]/(authed)/account/account-data.ts +5 -8
- package/templates/apps/web/app/[locale]/(authed)/account/inbox/message/[id]/file/__tests__/route.test.ts +205 -0
- package/templates/apps/web/app/[locale]/(authed)/account/inbox/message/[id]/file/route.ts +82 -0
- package/templates/apps/web/app/[locale]/(authed)/account/settings/page.tsx +98 -29
- package/templates/apps/web/app/[locale]/(authed)/account/support-actions.ts +6 -3
- package/templates/apps/web/app/[locale]/(authed)/layout.tsx +19 -101
- package/templates/apps/web/app/[locale]/(authed)/signed-out-gate.generated.tsx +90 -0
- package/templates/apps/web/app/[locale]/[slug]/[detail]/__tests__/page.test.tsx +3 -0
- package/templates/apps/web/app/[locale]/[slug]/__tests__/page.test.tsx +3 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +66 -14
- package/templates/apps/web/app/[locale]/contact/__tests__/site-contact-lines.test.ts +105 -11
- package/templates/apps/web/app/[locale]/contact/site-contact-lines.ts +12 -8
- package/templates/apps/web/app/[locale]/layout.tsx +83 -18
- package/templates/apps/web/app/[locale]/newsletter/__tests__/page.test.tsx +3 -0
- package/templates/apps/web/app/[locale]/projects/__tests__/page-renderer-migration.test.tsx +5 -2
- package/templates/apps/web/app/[locale]/thanks/[slug]/__tests__/page.test.tsx +3 -0
- package/templates/apps/web/app/api/auth/hub/authorize/__tests__/route.test.ts +295 -0
- package/templates/apps/web/app/api/auth/hub/authorize/route.ts +156 -0
- package/templates/apps/web/app/api/hub-session/__tests__/routes.test.ts +341 -0
- package/templates/apps/web/app/api/hub-session/callback/route.ts +168 -0
- package/templates/apps/web/app/api/hub-session/sign-in/route.ts +76 -0
- package/templates/apps/web/app/api/hub-session/sign-out/route.ts +63 -0
- package/templates/apps/web/app/openapi.json/__tests__/route.test.ts +104 -1
- package/templates/apps/web/app/openapi.json/route.ts +72 -7
- package/templates/apps/web/auth.ts +29 -2
- package/templates/apps/web/components/sign-out-trigger.tsx +20 -1
- package/templates/apps/web/components/signed-in-nav.tsx +17 -3
- package/templates/apps/web/instrumentation.ts +7 -4
- package/templates/apps/web/lib/__tests__/account-config.test.ts +5 -2
- package/templates/apps/web/lib/__tests__/closing-footer-path.test.ts +96 -0
- package/templates/apps/web/lib/__tests__/listings-config.test.ts +83 -0
- package/templates/apps/web/lib/__tests__/listings.test.tsx +49 -0
- package/templates/apps/web/lib/__tests__/meeting-slugs.test.ts +49 -1
- package/templates/apps/web/lib/__tests__/relay.test.ts +317 -0
- package/templates/apps/web/lib/__tests__/site-brand.test.ts +52 -0
- package/templates/apps/web/lib/account-config.ts +130 -1
- package/templates/apps/web/lib/api-origin.generated.ts +11 -0
- package/templates/apps/web/lib/closing-footer-path.ts +50 -0
- package/templates/apps/web/lib/hub-session/__tests__/hub-session.test.ts +262 -0
- package/templates/apps/web/lib/hub-session/config.ts +104 -0
- package/templates/apps/web/lib/hub-session/index.ts +23 -0
- package/templates/apps/web/lib/hub-session/seal.ts +82 -0
- package/templates/apps/web/lib/hub-session/session.ts +180 -0
- package/templates/apps/web/lib/hub-session/sign-in-gate.tsx +79 -0
- package/templates/apps/web/lib/listings-config.ts +31 -0
- package/templates/apps/web/lib/listings.tsx +7 -3
- package/templates/apps/web/lib/meeting-slugs.ts +11 -1
- package/templates/apps/web/lib/next-path.ts +35 -0
- package/templates/apps/web/lib/relay.ts +209 -3
- package/templates/apps/web/lib/server-api.ts +11 -1
- package/templates/apps/web/lib/site-brand.tsx +22 -1
- package/templates/apps/web/lib/site-chrome.ts +3 -0
- package/templates/apps/web/lib/topology.generated.ts +33 -0
- package/templates/apps/web/messages/en.json +32 -3
- package/templates/apps/web/package.json +1 -1
- package/templates/database/CHANGELOG.md +51 -0
- package/templates/database/__tests__/agents.db.test.ts +85 -0
- package/templates/database/__tests__/fixtures/person-backfill.json +264 -0
- package/templates/database/__tests__/lead-idempotency-key.db.test.ts +92 -0
- package/templates/database/__tests__/member-area.db.test.ts +245 -0
- package/templates/database/__tests__/person.db.test.ts +584 -0
- package/templates/database/auth/schema.prisma +24 -0
- package/templates/database/config/schema.prisma +13 -0
- package/templates/database/inbox/schema.prisma +111 -0
- package/templates/database/jest.config.cjs +7 -11
- package/templates/database/migrations/20261008120000_user_last_workspace/migration.sql +35 -0
- package/templates/database/migrations/20261008130000_lead_idempotency_key/migration.sql +20 -0
- package/templates/database/migrations/20261009120000_agent_grants/migration.sql +33 -0
- package/templates/database/migrations/20261010120000_person/migration.sql +106 -0
- package/templates/database/migrations/20261010121000_person_backfill/migration.sql +132 -0
- package/templates/database/migrations/20261010130000_agent_grants_people_read/migration.sql +20 -0
- package/templates/database/migrations/20261011120000_member_area/migration.sql +158 -0
- package/templates/database/migrations/20261011130000_agent_grants_member_pieces_manage/migration.sql +21 -0
- package/templates/database/package.json +1 -1
- package/templates/database/scripts/person-backfill.sql +132 -0
- package/templates/database/tenancy.ts +53 -0
- package/templates/tools/check-declared-env.ts +37 -0
- package/templates/tools/declared-env.ts +1163 -0
- package/versions.json +35 -34
|
@@ -17,6 +17,9 @@
|
|
|
17
17
|
* (#6228): the assertion's first arm now skips on any OTHER adapter, so a
|
|
18
18
|
* case that means to exercise the REAL resolution must say which adapter it
|
|
19
19
|
* is booting under rather than inherit whatever the ambient env holds.
|
|
20
|
+
*
|
|
21
|
+
* `describe("site stamp (#7868)")` covers the Checkout `metadata.site` stamp
|
|
22
|
+
* and the webhook's foreign-site test (spec D2/D7).
|
|
20
23
|
*/
|
|
21
24
|
|
|
22
25
|
// eslint-disable-next-line no-restricted-syntax -- lib/prisma.ts wraps the external DB boundary (PrismaClient requires a live connection); __mocks__/prisma.ts is the repo-standard test double for this boundary.
|
|
@@ -28,7 +31,9 @@ import {
|
|
|
28
31
|
__resetSiteWorkspaceCacheForTests,
|
|
29
32
|
assertSiteAliasResolves,
|
|
30
33
|
currentSiteId,
|
|
34
|
+
isForeignSiteStamp,
|
|
31
35
|
resolveSiteWorkspaceId,
|
|
36
|
+
siteStamp,
|
|
32
37
|
} from "../site.js";
|
|
33
38
|
|
|
34
39
|
const DEFAULT_ORG_ID = "org_00000000-0000-7000-8000-000000000001";
|
|
@@ -236,3 +241,52 @@ describe("assertSiteAliasResolves — the #6228 content-adapter carve-out (arm 0
|
|
|
236
241
|
expect(prisma.workspace.findUnique).toHaveBeenCalled();
|
|
237
242
|
});
|
|
238
243
|
});
|
|
244
|
+
|
|
245
|
+
describe("site stamp (#7868)", () => {
|
|
246
|
+
// Stripe types metadata values as strings; a non-string arrives only from a
|
|
247
|
+
// malformed payload, so the fixture is cast at this one boundary.
|
|
248
|
+
const nonStringStamp: Record<string, unknown> = { site: 42 };
|
|
249
|
+
|
|
250
|
+
it("siteStamp() with AILK_SITE_ID unset stamps the default alias", () => {
|
|
251
|
+
delete process.env.AILK_SITE_ID;
|
|
252
|
+
expect(siteStamp()).toEqual({ site: "default" });
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("siteStamp() follows AILK_SITE_ID, read per call", () => {
|
|
256
|
+
process.env.AILK_SITE_ID = PINNED_WORKSPACE_ID;
|
|
257
|
+
expect(siteStamp()).toEqual({ site: PINNED_WORKSPACE_ID });
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it("siteStamp() is empty when AILK_SITE_ID is the empty string — Stripe is never sent an empty value (SEC-7868-5)", () => {
|
|
261
|
+
process.env.AILK_SITE_ID = "";
|
|
262
|
+
expect(siteStamp()).toEqual({});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it.each([
|
|
266
|
+
["undefined metadata", undefined],
|
|
267
|
+
["null metadata", null],
|
|
268
|
+
["no stamp", {}],
|
|
269
|
+
["an empty stamp", { site: "" }],
|
|
270
|
+
["the own stamp", { site: "default" }],
|
|
271
|
+
["a non-string stamp", nonStringStamp as Record<string, string>],
|
|
272
|
+
])("isForeignSiteStamp is false for %s (unstamped is own, roadmap D4)", (_label, metadata) => {
|
|
273
|
+
delete process.env.AILK_SITE_ID;
|
|
274
|
+
expect(isForeignSiteStamp(metadata)).toBe(false);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it("isForeignSiteStamp is true for another site's stamp", () => {
|
|
278
|
+
delete process.env.AILK_SITE_ID;
|
|
279
|
+
expect(isForeignSiteStamp({ site: "samhenry" })).toBe(true);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it("isForeignSiteStamp compares exactly — a case-folded own alias is foreign", () => {
|
|
283
|
+
delete process.env.AILK_SITE_ID;
|
|
284
|
+
expect(isForeignSiteStamp({ site: "Default" })).toBe(true);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it("isForeignSiteStamp reads the alias per call, not once at import", () => {
|
|
288
|
+
process.env.AILK_SITE_ID = PINNED_WORKSPACE_ID;
|
|
289
|
+
expect(isForeignSiteStamp({ site: PINNED_WORKSPACE_ID })).toBe(false);
|
|
290
|
+
expect(isForeignSiteStamp({ site: "default" })).toBe(true);
|
|
291
|
+
});
|
|
292
|
+
});
|
|
@@ -19,6 +19,7 @@ import { createHash } from "node:crypto";
|
|
|
19
19
|
import { prisma } from "../prisma.js";
|
|
20
20
|
import {
|
|
21
21
|
SPOKE_KEY_PREFIX,
|
|
22
|
+
findSpokeById,
|
|
22
23
|
findSpokeByKeyHash,
|
|
23
24
|
hasRegisteredSpoke,
|
|
24
25
|
hashSpokeKey,
|
|
@@ -79,6 +80,25 @@ describe("findSpokeByKeyHash (D3 step 5)", () => {
|
|
|
79
80
|
});
|
|
80
81
|
});
|
|
81
82
|
|
|
83
|
+
describe("findSpokeById (#7894 D3, D5)", () => {
|
|
84
|
+
it("is one findUnique by id, selecting the row the sign-in client needs", async () => {
|
|
85
|
+
const row = { id: S1, workspaceId: W1, revokedAt: null, name: "Site" };
|
|
86
|
+
db.spoke.findUnique.mockResolvedValue(row as never);
|
|
87
|
+
|
|
88
|
+
await expect(findSpokeById(S1)).resolves.toEqual(row);
|
|
89
|
+
expect(db.spoke.findUnique).toHaveBeenCalledTimes(1);
|
|
90
|
+
expect(db.spoke.findUnique).toHaveBeenCalledWith({
|
|
91
|
+
where: { id: S1 },
|
|
92
|
+
select: { id: true, workspaceId: true, revokedAt: true, name: true },
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("answers null for an unknown id", async () => {
|
|
97
|
+
db.spoke.findUnique.mockResolvedValue(null);
|
|
98
|
+
await expect(findSpokeById(S1)).resolves.toBeNull();
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
82
102
|
describe("hasRegisteredSpoke (#7633 D1, AC-5)", () => {
|
|
83
103
|
it("is true when the registry holds a row, revoked or not — one findFirst with no where", async () => {
|
|
84
104
|
db.spoke.findFirst.mockResolvedValue({ id: S1 } as never);
|
|
@@ -27,13 +27,25 @@
|
|
|
27
27
|
* `findAgentByKeyHash` and `touchAgentLastActive` only;
|
|
28
28
|
* - the management routes (`routes/workspaces/agents.ts`) call the rest.
|
|
29
29
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
30
|
+
* GRANTS (#7864, spec docs/specs/7864-agent-key-grants-spec.md D2–D4). An
|
|
31
|
+
* agent's row carries the permissions an owner or admin granted it one by one
|
|
32
|
+
* — only the five `AGENT_GRANTABLE_PERMISSIONS`. `setServedAgentGrants` is
|
|
33
|
+
* the column's only writer, and refuses a grant its person's own role does
|
|
34
|
+
* not hold; the tenant guards apply the same rule on every request, so a
|
|
35
|
+
* grant stored before a demotion is held to the person's current role.
|
|
36
|
+
*
|
|
37
|
+
* Nothing caches a row: a switch-off, a re-key, a grant change or a removed
|
|
38
|
+
* membership takes effect on the next request.
|
|
32
39
|
*/
|
|
33
40
|
import { createHash, randomBytes } from "node:crypto";
|
|
34
41
|
|
|
35
42
|
import type { Id } from "@working-theory/ids";
|
|
36
|
-
import
|
|
43
|
+
import {
|
|
44
|
+
AGENT_GRANTABLE_PERMISSIONS,
|
|
45
|
+
can,
|
|
46
|
+
type AgentGrantablePermission,
|
|
47
|
+
type AgentPermissionLevel,
|
|
48
|
+
} from "@working-theory/validation";
|
|
37
49
|
|
|
38
50
|
import { prisma } from "./prisma.js";
|
|
39
51
|
|
|
@@ -71,6 +83,21 @@ export interface AgentGrant {
|
|
|
71
83
|
organizationId: Id<"organization">;
|
|
72
84
|
responsibleMembershipId: Id<"membership">;
|
|
73
85
|
permissionLevel: AgentPermissionLevel;
|
|
86
|
+
/**
|
|
87
|
+
* The grants stored on the row (#7864 D3). The tenant guards keep only
|
|
88
|
+
* those its person's role also holds (`agentTenant`).
|
|
89
|
+
*/
|
|
90
|
+
grants: readonly string[];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* A stored grant set in `AGENT_GRANTABLE_PERMISSIONS` order, without
|
|
95
|
+
* duplicates and without anything outside the five.
|
|
96
|
+
*/
|
|
97
|
+
function orderedGrants(
|
|
98
|
+
grants: readonly string[],
|
|
99
|
+
): AgentGrantablePermission[] {
|
|
100
|
+
return AGENT_GRANTABLE_PERMISSIONS.filter((p) => grants.includes(p));
|
|
74
101
|
}
|
|
75
102
|
|
|
76
103
|
// ── The auth preHandler's three functions ────────────────────────────────────
|
|
@@ -80,6 +107,8 @@ export interface AgentKeyRow {
|
|
|
80
107
|
workspaceId: string;
|
|
81
108
|
organizationId: string;
|
|
82
109
|
permissionLevel: AgentPermissionLevel;
|
|
110
|
+
/** `undefined` only from a test double; the auth preHandler reads it as none. */
|
|
111
|
+
grants: string[] | undefined;
|
|
83
112
|
enabled: boolean;
|
|
84
113
|
keyIssuedToMembershipId: string | null;
|
|
85
114
|
lastActiveAt: Date | null;
|
|
@@ -97,6 +126,7 @@ export async function findAgentByKeyHash(
|
|
|
97
126
|
workspaceId: true,
|
|
98
127
|
organizationId: true,
|
|
99
128
|
permissionLevel: true,
|
|
129
|
+
grants: true,
|
|
100
130
|
enabled: true,
|
|
101
131
|
keyIssuedToMembershipId: true,
|
|
102
132
|
lastActiveAt: true,
|
|
@@ -180,6 +210,12 @@ interface SetAgentEnabledParams {
|
|
|
180
210
|
enabled: boolean;
|
|
181
211
|
}
|
|
182
212
|
|
|
213
|
+
interface SetAgentGrantsParams {
|
|
214
|
+
organizationId: string;
|
|
215
|
+
agentId: string;
|
|
216
|
+
grants: readonly AgentGrantablePermission[];
|
|
217
|
+
}
|
|
218
|
+
|
|
183
219
|
/**
|
|
184
220
|
* Add an agent for `responsibleUserId`, bound to `workspaceId`. The returned
|
|
185
221
|
* `key` is the only copy that will ever exist: the caller shows it once.
|
|
@@ -280,6 +316,58 @@ export async function setServedAgentEnabled(
|
|
|
280
316
|
});
|
|
281
317
|
}
|
|
282
318
|
|
|
319
|
+
/** A grant set the agent's person's own role does not wholly hold. */
|
|
320
|
+
export type AgentGrantExceedsPerson = { ok: false; reason: "exceeds-person" };
|
|
321
|
+
export type ServedAgentGrants = ServedAgent & {
|
|
322
|
+
/** The grants stored before this write, in D1 order — for the audit row. */
|
|
323
|
+
previous: AgentGrantablePermission[];
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Replace an agent's grants with `grants` (#7864 D4) and answer with its list
|
|
328
|
+
* row. One transaction: the agent is read by id inside the caller's
|
|
329
|
+
* organization, with its responsible person's role; any requested grant that
|
|
330
|
+
* role does not hold refuses the whole set (`exceeds-person`) and writes
|
|
331
|
+
* nothing. An agent of another organization, or none, is `not-found`.
|
|
332
|
+
*/
|
|
333
|
+
export async function setServedAgentGrants(
|
|
334
|
+
params: SetAgentGrantsParams,
|
|
335
|
+
): Promise<ServedAgentGrants | AgentNotFound | AgentGrantExceedsPerson> {
|
|
336
|
+
return prisma.$transaction(async (tx) => {
|
|
337
|
+
const current = await tx.agent.findUnique({
|
|
338
|
+
where: agentScope(params),
|
|
339
|
+
select: {
|
|
340
|
+
organizationId: true,
|
|
341
|
+
grants: true,
|
|
342
|
+
responsible: { select: { role: true } },
|
|
343
|
+
},
|
|
344
|
+
});
|
|
345
|
+
if (!current || current.organizationId !== params.organizationId) {
|
|
346
|
+
return { ok: false, reason: "not-found" } as const;
|
|
347
|
+
}
|
|
348
|
+
const grants = orderedGrants(params.grants);
|
|
349
|
+
if (!grants.every((grant) => can(current.responsible.role, grant))) {
|
|
350
|
+
return { ok: false, reason: "exceeds-person" } as const;
|
|
351
|
+
}
|
|
352
|
+
const { count } = await tx.agent.updateMany({
|
|
353
|
+
where: agentScope(params),
|
|
354
|
+
data: { grants },
|
|
355
|
+
});
|
|
356
|
+
if (count !== 1) return { ok: false, reason: "not-found" } as const;
|
|
357
|
+
const [agent] = await readAgentListRows(
|
|
358
|
+
{ id: params.agentId, organizationId: params.organizationId },
|
|
359
|
+
tx,
|
|
360
|
+
);
|
|
361
|
+
return agent
|
|
362
|
+
? ({
|
|
363
|
+
ok: true,
|
|
364
|
+
agent,
|
|
365
|
+
previous: orderedGrants(current.grants),
|
|
366
|
+
} as const)
|
|
367
|
+
: ({ ok: false, reason: "not-found" } as const);
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
|
|
283
371
|
/** The list row of a just-written agent, read on the write's own client. */
|
|
284
372
|
async function servedWith(
|
|
285
373
|
db: AgentDb,
|
|
@@ -392,6 +480,8 @@ export interface AgentListRow {
|
|
|
392
480
|
workspaceId: string;
|
|
393
481
|
responsible: { userId: string; name: string | null; email: string };
|
|
394
482
|
permissionLevel: AgentPermissionLevel;
|
|
483
|
+
/** The stored grants (#7864 D4), in `AGENT_GRANTABLE_PERMISSIONS` order. */
|
|
484
|
+
grants: AgentGrantablePermission[];
|
|
395
485
|
enabled: boolean;
|
|
396
486
|
lastActiveAt: Date | null;
|
|
397
487
|
createdAt: Date;
|
|
@@ -431,6 +521,7 @@ async function readAgentListRows(
|
|
|
431
521
|
name: true,
|
|
432
522
|
workspaceId: true,
|
|
433
523
|
permissionLevel: true,
|
|
524
|
+
grants: true,
|
|
434
525
|
enabled: true,
|
|
435
526
|
lastActiveAt: true,
|
|
436
527
|
createdAt: true,
|
|
@@ -466,6 +557,7 @@ async function readAgentListRows(
|
|
|
466
557
|
email: r.responsible.user.email,
|
|
467
558
|
},
|
|
468
559
|
permissionLevel: r.permissionLevel,
|
|
560
|
+
grants: orderedGrants(r.grants),
|
|
469
561
|
enabled: r.enabled,
|
|
470
562
|
lastActiveAt: r.lastActiveAt,
|
|
471
563
|
createdAt: r.createdAt,
|
|
@@ -23,12 +23,29 @@
|
|
|
23
23
|
* (D15), so a rewritten or mis-set grant can never serve another
|
|
24
24
|
* workspace's bytes.
|
|
25
25
|
*
|
|
26
|
+
* Neither ever resolves a client file (SEC-7877-A, #7877): an asset tagged
|
|
27
|
+
* `private`, or one a member-area message attaches, is the same `null` a
|
|
28
|
+
* miss is — so a magnet pointed at a client file's key mints nothing and a
|
|
29
|
+
* grant minted before the file became one serves nothing.
|
|
30
|
+
*
|
|
26
31
|
* No write, no `findMany`, no list, no unscoped read — both functions take
|
|
27
32
|
* a `workspaceId` the caller resolved server-side, never a client-supplied
|
|
28
33
|
* value, so neither can enumerate or cross a tenant boundary (NFR3).
|
|
29
34
|
*/
|
|
35
|
+
import { PRIVATE_ASSET_TAG } from "@working-theory/validation";
|
|
36
|
+
|
|
30
37
|
import { prisma } from "./prisma.js";
|
|
31
38
|
|
|
39
|
+
/** A client file (SEC-7877-A) — `private`-tagged, or attached to a message. */
|
|
40
|
+
async function isClientFile(
|
|
41
|
+
assetId: string,
|
|
42
|
+
workspaceId: string,
|
|
43
|
+
tags: string[],
|
|
44
|
+
): Promise<boolean> {
|
|
45
|
+
if (tags.includes(PRIVATE_ASSET_TAG)) return true;
|
|
46
|
+
return (await prisma.interaction.count({ where: { assetId, workspaceId } })) > 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
32
49
|
/** What the mint-time resolution needs, and nothing more. */
|
|
33
50
|
export async function findDeliverableAssetIdByKey(
|
|
34
51
|
workspaceId: string,
|
|
@@ -36,9 +53,10 @@ export async function findDeliverableAssetIdByKey(
|
|
|
36
53
|
): Promise<string | null> {
|
|
37
54
|
const row = await prisma.asset.findFirst({
|
|
38
55
|
where: { workspaceId, key },
|
|
39
|
-
select: { id: true },
|
|
56
|
+
select: { id: true, tags: true },
|
|
40
57
|
});
|
|
41
|
-
|
|
58
|
+
if (!row || (await isClientFile(row.id, workspaceId, row.tags))) return null;
|
|
59
|
+
return row.id;
|
|
42
60
|
}
|
|
43
61
|
|
|
44
62
|
/** What the serve path needs, and deliberately nothing more (no key, no workspace). */
|
|
@@ -61,12 +79,13 @@ export async function readDeliverableAsset(
|
|
|
61
79
|
|
|
62
80
|
const row = await prisma.asset.findFirst({
|
|
63
81
|
where: { id: assetId, workspaceId },
|
|
64
|
-
select: { filename: true, contentType: true, bytes: true },
|
|
82
|
+
select: { filename: true, contentType: true, bytes: true, tags: true },
|
|
65
83
|
});
|
|
66
|
-
if (!row) return null;
|
|
84
|
+
if (!row || (await isClientFile(assetId, workspaceId, row.tags))) return null;
|
|
67
85
|
|
|
68
86
|
return {
|
|
69
|
-
|
|
87
|
+
filename: row.filename,
|
|
88
|
+
contentType: row.contentType,
|
|
70
89
|
// Prisma maps a `Bytes` column to `Uint8Array`, and Fastify treats only
|
|
71
90
|
// a `Buffer` as a raw payload — this wraps rather than copies
|
|
72
91
|
// (`Buffer.from(buffer, byteOffset, length)` is a view), mirroring
|