@stigmer/server 3.39.1-dev.20260928194002 → 3.40.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/dist/artifactstorage/artifact-storage.d.ts +10 -2
- package/dist/artifactstorage/artifact-storage.d.ts.map +1 -1
- package/dist/artifactstorage/artifact-storage.js +6 -3
- package/dist/artifactstorage/artifact-storage.js.map +1 -1
- package/dist/boot/artifact-lane.d.ts +55 -0
- package/dist/boot/artifact-lane.d.ts.map +1 -0
- package/dist/boot/artifact-lane.js +74 -0
- package/dist/boot/artifact-lane.js.map +1 -0
- package/dist/boot/compose.d.ts.map +1 -1
- package/dist/boot/compose.js +43 -19
- package/dist/boot/compose.js.map +1 -1
- package/dist/boot/config.d.ts +11 -6
- package/dist/boot/config.d.ts.map +1 -1
- package/dist/boot/config.js +10 -5
- package/dist/boot/config.js.map +1 -1
- package/dist/domain/agent/controller.d.ts +5 -2
- package/dist/domain/agent/controller.d.ts.map +1 -1
- package/dist/domain/agent/controller.js.map +1 -1
- package/dist/domain/agentexecution/controller.d.ts +18 -4
- package/dist/domain/agentexecution/controller.d.ts.map +1 -1
- package/dist/domain/agentexecution/controller.js +2 -2
- package/dist/domain/agentexecution/controller.js.map +1 -1
- package/dist/domain/agentexecution/create-steps.d.ts +29 -15
- package/dist/domain/agentexecution/create-steps.d.ts.map +1 -1
- package/dist/domain/agentexecution/create-steps.js +118 -51
- package/dist/domain/agentexecution/create-steps.js.map +1 -1
- package/dist/domain/agentinstance/controller.d.ts +8 -2
- package/dist/domain/agentinstance/controller.d.ts.map +1 -1
- package/dist/domain/agentinstance/controller.js.map +1 -1
- package/dist/domain/agentshare/controller.d.ts +9 -4
- package/dist/domain/agentshare/controller.d.ts.map +1 -1
- package/dist/domain/agentshare/controller.js +3 -3
- package/dist/domain/agentshare/controller.js.map +1 -1
- package/dist/domain/artifact/file-server.d.ts +2 -1
- package/dist/domain/artifact/file-server.d.ts.map +1 -1
- package/dist/domain/artifact/file-server.js +6 -3
- package/dist/domain/artifact/file-server.js.map +1 -1
- package/dist/domain/channelapp/controller.d.ts +12 -3
- package/dist/domain/channelapp/controller.d.ts.map +1 -1
- package/dist/domain/channelapp/controller.js +19 -9
- package/dist/domain/channelapp/controller.js.map +1 -1
- package/dist/domain/environment/controller.d.ts +8 -5
- package/dist/domain/environment/controller.d.ts.map +1 -1
- package/dist/domain/environment/controller.js +3 -3
- package/dist/domain/environment/controller.js.map +1 -1
- package/dist/domain/executioncontext/controller.d.ts +7 -4
- package/dist/domain/executioncontext/controller.d.ts.map +1 -1
- package/dist/domain/executioncontext/controller.js.map +1 -1
- package/dist/domain/mcpserver/controller.d.ts.map +1 -1
- package/dist/domain/mcpserver/controller.js +6 -2
- package/dist/domain/mcpserver/controller.js.map +1 -1
- package/dist/domain/memory/constants.d.ts +9 -0
- package/dist/domain/memory/constants.d.ts.map +1 -1
- package/dist/domain/memory/constants.js +9 -0
- package/dist/domain/memory/constants.js.map +1 -1
- package/dist/domain/memory/controller.d.ts +18 -6
- package/dist/domain/memory/controller.d.ts.map +1 -1
- package/dist/domain/memory/controller.js +18 -21
- package/dist/domain/memory/controller.js.map +1 -1
- package/dist/domain/memory/steps.d.ts +38 -10
- package/dist/domain/memory/steps.d.ts.map +1 -1
- package/dist/domain/memory/steps.js +87 -17
- package/dist/domain/memory/steps.js.map +1 -1
- package/dist/domain/organization/controller.d.ts +7 -4
- package/dist/domain/organization/controller.d.ts.map +1 -1
- package/dist/domain/organization/controller.js.map +1 -1
- package/dist/domain/session/controller.d.ts +7 -3
- package/dist/domain/session/controller.d.ts.map +1 -1
- package/dist/domain/session/controller.js.map +1 -1
- package/dist/domain/skill/controller.d.ts.map +1 -1
- package/dist/domain/skill/controller.js +8 -2
- package/dist/domain/skill/controller.js.map +1 -1
- package/dist/domain/workflow/controller.d.ts +6 -2
- package/dist/domain/workflow/controller.d.ts.map +1 -1
- package/dist/domain/workflow/controller.js.map +1 -1
- package/dist/domain/workflowexecution/controller.d.ts +8 -4
- package/dist/domain/workflowexecution/controller.d.ts.map +1 -1
- package/dist/domain/workflowexecution/controller.js.map +1 -1
- package/dist/domain/workflowexecution/update-status.d.ts.map +1 -1
- package/dist/domain/workflowexecution/update-status.js +3 -2
- package/dist/domain/workflowexecution/update-status.js.map +1 -1
- package/dist/extensions/identity.d.ts +28 -14
- package/dist/extensions/identity.d.ts.map +1 -1
- package/dist/extensions/identity.js +38 -0
- package/dist/extensions/identity.js.map +1 -1
- package/dist/pipeline/steps/guard-memory-capture.d.ts.map +1 -1
- package/dist/pipeline/steps/guard-memory-capture.js +1 -13
- package/dist/pipeline/steps/guard-memory-capture.js.map +1 -1
- package/package.json +6 -6
- package/src/artifactstorage/__tests__/artifact-storage.test.ts +15 -1
- package/src/artifactstorage/artifact-storage.ts +12 -4
- package/src/boot/__tests__/artifact-lane.test.ts +71 -0
- package/src/boot/__tests__/compose.test.ts +73 -3
- package/src/boot/__tests__/config.test.ts +19 -0
- package/src/boot/artifact-lane.ts +96 -0
- package/src/boot/compose.ts +58 -21
- package/src/boot/config.ts +25 -15
- package/src/domain/agent/controller.ts +5 -2
- package/src/domain/agentexecution/__tests__/create-steps.test.ts +230 -2
- package/src/domain/agentexecution/controller.ts +32 -6
- package/src/domain/agentexecution/create-steps.ts +162 -62
- package/src/domain/agentinstance/controller.ts +8 -2
- package/src/domain/agentshare/controller.ts +12 -7
- package/src/domain/artifact/file-server.ts +8 -4
- package/src/domain/channelapp/controller.ts +38 -11
- package/src/domain/environment/controller.ts +11 -8
- package/src/domain/executioncontext/controller.ts +7 -4
- package/src/domain/mcpserver/controller.ts +6 -2
- package/src/domain/memory/__tests__/capture-credential.test.ts +6 -6
- package/src/domain/memory/__tests__/person-consent-composed.test.ts +187 -0
- package/src/domain/memory/__tests__/person-consent.test.ts +274 -0
- package/src/domain/memory/constants.ts +11 -0
- package/src/domain/memory/controller.ts +36 -27
- package/src/domain/memory/steps.ts +112 -17
- package/src/domain/organization/controller.ts +7 -4
- package/src/domain/plugin/__tests__/plugin.test.ts +8 -0
- package/src/domain/session/controller.ts +7 -3
- package/src/domain/skill/__tests__/skill.test.ts +4 -0
- package/src/domain/skill/controller.ts +8 -2
- package/src/domain/workflow/controller.ts +6 -2
- package/src/domain/workflowexecution/controller.ts +8 -4
- package/src/domain/workflowexecution/update-status.ts +3 -2
- package/src/extensions/__tests__/identity.test.ts +82 -0
- package/src/extensions/__tests__/list-read-scope-composed.test.ts +39 -1
- package/src/extensions/identity.ts +47 -0
- package/src/pipeline/steps/guard-memory-capture.ts +4 -19
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pins memory capture where callers are persons (stigmer#1387): with the
|
|
3
|
+
* identity-account directory composed, ResolveMemoryDefaults files the
|
|
4
|
+
* memory under the id of the account it is about — the admitted capture
|
|
5
|
+
* credential's subject (an account id or a raw issuer subject, read the
|
|
6
|
+
* way accountForStamp reads a creator stamp), else the calling person —
|
|
7
|
+
* and hands that account to CheckMemoryEnablement, which requires the
|
|
8
|
+
* organization's switch and then the person's own (the Java
|
|
9
|
+
* MemoryCreateHandler's order and copy). A caller no account stands for is
|
|
10
|
+
* stamped "" and refused by the enablement step, behind the authorization
|
|
11
|
+
* bar. Without the directory — the single-operator posture — the org flag
|
|
12
|
+
* alone decides and the subject stays the "" sentinel.
|
|
13
|
+
*/
|
|
14
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
15
|
+
import { tmpdir } from "node:os";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
|
|
18
|
+
import { create } from "@bufbuild/protobuf";
|
|
19
|
+
import { Code, ConnectError } from "@connectrpc/connect";
|
|
20
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
21
|
+
|
|
22
|
+
import { MemorySchema } from "@stigmer/protos/ai/stigmer/agentic/memory/v1/api_pb";
|
|
23
|
+
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
24
|
+
import { IdentityAccountSchema } from "@stigmer/protos/ai/stigmer/iam/identityaccount/v1/api_pb";
|
|
25
|
+
import { OrganizationSchema } from "@stigmer/protos/ai/stigmer/tenancy/organization/v1/api_pb";
|
|
26
|
+
|
|
27
|
+
import type { CallerIdentity } from "../../../extensions/identity.js";
|
|
28
|
+
import { RequestContext } from "../../../pipeline/request-context.js";
|
|
29
|
+
import { MEMORY_CAPTURE_CREDENTIAL_KEY } from "../../../pipeline/steps/guard-memory-capture.js";
|
|
30
|
+
import type { Store } from "../../../store/interface.js";
|
|
31
|
+
import { SqliteStore } from "../../../store/sqlite/store.js";
|
|
32
|
+
import type { AccountsByCaller } from "../../identityaccount/resolve.js";
|
|
33
|
+
import { fakeIdentityAccountStore } from "../../identityaccount/__tests__/support.js";
|
|
34
|
+
import type { FakeIdentityAccountStore } from "../../identityaccount/__tests__/support.js";
|
|
35
|
+
import {
|
|
36
|
+
MEMORY_ACCOUNT_DISABLED_MESSAGE,
|
|
37
|
+
memoryDisabledMessage,
|
|
38
|
+
} from "../constants.js";
|
|
39
|
+
import {
|
|
40
|
+
memorySubjectAccountOf,
|
|
41
|
+
newCheckMemoryEnablementStep,
|
|
42
|
+
newResolveMemoryDefaultsStep,
|
|
43
|
+
} from "../steps.js";
|
|
44
|
+
|
|
45
|
+
const ORG = "org_acme";
|
|
46
|
+
const CAROL = "ida_carol";
|
|
47
|
+
const CAROL_SUBJECT = "auth0|carol";
|
|
48
|
+
|
|
49
|
+
/** Carol at the console: a wire user whose identity is her account id. */
|
|
50
|
+
const CAROL_AT_CONSOLE: CallerIdentity = {
|
|
51
|
+
identityId: CAROL,
|
|
52
|
+
callerClass: "user",
|
|
53
|
+
issuer: "https://issuer.example",
|
|
54
|
+
rawToken: "opaque",
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/** A sandbox-lane caller; its capture credential names the subject. */
|
|
58
|
+
const SANDBOX: CallerIdentity = {
|
|
59
|
+
identityId: CAROL,
|
|
60
|
+
callerClass: "sandbox",
|
|
61
|
+
issuer: "stigmer",
|
|
62
|
+
rawToken: "opaque",
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/** A signed-in caller no account stands for yet (idp-shaped). */
|
|
66
|
+
const STRANGER: CallerIdentity = {
|
|
67
|
+
identityId: "auth0|stranger",
|
|
68
|
+
callerClass: "user",
|
|
69
|
+
issuer: "https://issuer.example",
|
|
70
|
+
rawToken: "opaque",
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
let dir: string;
|
|
74
|
+
let store: Store;
|
|
75
|
+
let accounts: FakeIdentityAccountStore;
|
|
76
|
+
|
|
77
|
+
beforeEach(async () => {
|
|
78
|
+
dir = mkdtempSync(path.join(tmpdir(), "memory-person-consent-"));
|
|
79
|
+
store = SqliteStore.open(path.join(dir, "stigmer.db"));
|
|
80
|
+
accounts = fakeIdentityAccountStore();
|
|
81
|
+
seedCarol({ memoryEnabled: true });
|
|
82
|
+
await seedOrg(true);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
afterEach(() => {
|
|
86
|
+
rmSync(dir, { recursive: true, force: true });
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
function seedCarol(preferences: { memoryEnabled: boolean }): void {
|
|
90
|
+
accounts.rows.set(
|
|
91
|
+
CAROL,
|
|
92
|
+
create(IdentityAccountSchema, {
|
|
93
|
+
metadata: { id: CAROL, name: "Carol" },
|
|
94
|
+
spec: { idpId: CAROL_SUBJECT, preferences },
|
|
95
|
+
}),
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function seedOrg(memoryEnabled: boolean): Promise<void> {
|
|
100
|
+
await store.saveResource(
|
|
101
|
+
ApiResourceKind.organization,
|
|
102
|
+
ORG,
|
|
103
|
+
OrganizationSchema,
|
|
104
|
+
create(OrganizationSchema, {
|
|
105
|
+
metadata: { id: ORG, name: ORG, org: ORG },
|
|
106
|
+
spec: { preferences: { memoryEnabled } },
|
|
107
|
+
}),
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function memoryCtx(
|
|
112
|
+
caller: CallerIdentity,
|
|
113
|
+
): RequestContext<typeof MemorySchema> {
|
|
114
|
+
return new RequestContext(
|
|
115
|
+
MemorySchema,
|
|
116
|
+
create(MemorySchema, {
|
|
117
|
+
metadata: { name: "m", org: ORG },
|
|
118
|
+
// A forged subject: the server always overwrites it.
|
|
119
|
+
spec: { content: "fact", subjectIdentityAccountId: "ida_forged" },
|
|
120
|
+
}),
|
|
121
|
+
caller,
|
|
122
|
+
ApiResourceKind.memory,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function withCredential(
|
|
127
|
+
ctx: RequestContext<typeof MemorySchema>,
|
|
128
|
+
subject: string,
|
|
129
|
+
): RequestContext<typeof MemorySchema> {
|
|
130
|
+
ctx.set(MEMORY_CAPTURE_CREDENTIAL_KEY, {
|
|
131
|
+
subjectIdentityAccountId: subject,
|
|
132
|
+
provedSessionId: "ses_proved",
|
|
133
|
+
});
|
|
134
|
+
return ctx;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Runs the create chain's two consent steps in their chain order. */
|
|
138
|
+
async function capture(
|
|
139
|
+
ctx: RequestContext<typeof MemorySchema>,
|
|
140
|
+
personAccounts: AccountsByCaller | undefined,
|
|
141
|
+
): Promise<void> {
|
|
142
|
+
await newResolveMemoryDefaultsStep(personAccounts).execute(ctx);
|
|
143
|
+
await newCheckMemoryEnablementStep(store, personAccounts).execute(ctx);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function refusalOf(run: Promise<void>): Promise<ConnectError> {
|
|
147
|
+
try {
|
|
148
|
+
await run;
|
|
149
|
+
} catch (error) {
|
|
150
|
+
return ConnectError.from(error);
|
|
151
|
+
}
|
|
152
|
+
throw new Error("expected the capture to be refused");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
describe("capture where callers are persons", () => {
|
|
156
|
+
it("files a direct capture under the calling person's account", async () => {
|
|
157
|
+
const ctx = memoryCtx(CAROL_AT_CONSOLE);
|
|
158
|
+
await capture(ctx, accounts);
|
|
159
|
+
expect(ctx.newState.spec?.subjectIdentityAccountId).toBe(CAROL);
|
|
160
|
+
expect(memorySubjectAccountOf(ctx)?.metadata?.id).toBe(CAROL);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("files a credentialed capture under the credential's subject", async () => {
|
|
164
|
+
const ctx = withCredential(memoryCtx(SANDBOX), CAROL);
|
|
165
|
+
await capture(ctx, accounts);
|
|
166
|
+
expect(ctx.newState.spec?.subjectIdentityAccountId).toBe(CAROL);
|
|
167
|
+
expect(ctx.newState.spec?.provenance?.sessionId).toBe("ses_proved");
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("normalises a credential subject that arrived as a raw issuer subject to the account id", async () => {
|
|
171
|
+
const ctx = withCredential(memoryCtx(SANDBOX), CAROL_SUBJECT);
|
|
172
|
+
await capture(ctx, accounts);
|
|
173
|
+
expect(ctx.newState.spec?.subjectIdentityAccountId).toBe(CAROL);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("refuses with the person's copy when their own switch is off", async () => {
|
|
177
|
+
seedCarol({ memoryEnabled: false });
|
|
178
|
+
for (const ctx of [
|
|
179
|
+
memoryCtx(CAROL_AT_CONSOLE),
|
|
180
|
+
withCredential(memoryCtx(SANDBOX), CAROL),
|
|
181
|
+
]) {
|
|
182
|
+
const refusal = await refusalOf(capture(ctx, accounts));
|
|
183
|
+
expect(refusal.code).toBe(Code.FailedPrecondition);
|
|
184
|
+
expect(refusal.rawMessage).toBe(MEMORY_ACCOUNT_DISABLED_MESSAGE);
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("asks the organization first: its switch off refuses with the org's copy", async () => {
|
|
189
|
+
seedCarol({ memoryEnabled: false });
|
|
190
|
+
await seedOrg(false);
|
|
191
|
+
const refusal = await refusalOf(
|
|
192
|
+
capture(memoryCtx(CAROL_AT_CONSOLE), accounts),
|
|
193
|
+
);
|
|
194
|
+
expect(refusal.code).toBe(Code.FailedPrecondition);
|
|
195
|
+
expect(refusal.rawMessage).toBe(memoryDisabledMessage(ORG));
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("stamps nobody and refuses in the enablement step when no account stands for the caller", async () => {
|
|
199
|
+
const ctx = memoryCtx(STRANGER);
|
|
200
|
+
await newResolveMemoryDefaultsStep(accounts).execute(ctx);
|
|
201
|
+
// Not refused here: the refusal belongs behind the authorization bar.
|
|
202
|
+
expect(ctx.newState.spec?.subjectIdentityAccountId).toBe("");
|
|
203
|
+
expect(memorySubjectAccountOf(ctx)).toBeUndefined();
|
|
204
|
+
const refusal = await refusalOf(
|
|
205
|
+
Promise.resolve(
|
|
206
|
+
newCheckMemoryEnablementStep(store, accounts).execute(ctx),
|
|
207
|
+
),
|
|
208
|
+
);
|
|
209
|
+
expect(refusal.code).toBe(Code.FailedPrecondition);
|
|
210
|
+
expect(refusal.rawMessage).toBe(MEMORY_ACCOUNT_DISABLED_MESSAGE);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("refuses a credential whose subject names no account", async () => {
|
|
214
|
+
const ctx = withCredential(memoryCtx(SANDBOX), "chn_org_acme");
|
|
215
|
+
const refusal = await refusalOf(capture(ctx, accounts));
|
|
216
|
+
expect(refusal.code).toBe(Code.FailedPrecondition);
|
|
217
|
+
expect(ctx.newState.spec?.subjectIdentityAccountId).toBe("");
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("answers a directory fault as an infrastructure fault, never a refusal", async () => {
|
|
221
|
+
const faulting: AccountsByCaller = {
|
|
222
|
+
findById: async () => {
|
|
223
|
+
throw new Error("simulated store fault");
|
|
224
|
+
},
|
|
225
|
+
findDirectByIdpId: async () => {
|
|
226
|
+
throw new Error("simulated store fault");
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
const refusal = await refusalOf(
|
|
230
|
+
Promise.resolve(
|
|
231
|
+
newResolveMemoryDefaultsStep(faulting).execute(
|
|
232
|
+
memoryCtx(CAROL_AT_CONSOLE),
|
|
233
|
+
),
|
|
234
|
+
),
|
|
235
|
+
);
|
|
236
|
+
expect(refusal.code).toBe(Code.Internal);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("refuses to file under an account row that carries no id", async () => {
|
|
240
|
+
const nameless: AccountsByCaller = {
|
|
241
|
+
findById: async () =>
|
|
242
|
+
create(IdentityAccountSchema, {
|
|
243
|
+
spec: { preferences: { memoryEnabled: true } },
|
|
244
|
+
}),
|
|
245
|
+
findDirectByIdpId: async () => undefined,
|
|
246
|
+
};
|
|
247
|
+
const refusal = await refusalOf(
|
|
248
|
+
Promise.resolve(
|
|
249
|
+
newResolveMemoryDefaultsStep(nameless).execute(
|
|
250
|
+
memoryCtx(CAROL_AT_CONSOLE),
|
|
251
|
+
),
|
|
252
|
+
),
|
|
253
|
+
);
|
|
254
|
+
expect(refusal.code).toBe(Code.Internal);
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
describe("capture under the single-operator posture", () => {
|
|
259
|
+
it("keeps the sentinel subject and the org flag alone, whatever the account says", async () => {
|
|
260
|
+
seedCarol({ memoryEnabled: false });
|
|
261
|
+
const ctx = memoryCtx(CAROL_AT_CONSOLE);
|
|
262
|
+
await capture(ctx, undefined);
|
|
263
|
+
expect(ctx.newState.spec?.subjectIdentityAccountId).toBe("");
|
|
264
|
+
expect(memorySubjectAccountOf(ctx)).toBeUndefined();
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("still refuses with the org's copy when the organization is off", async () => {
|
|
268
|
+
await seedOrg(false);
|
|
269
|
+
const refusal = await refusalOf(
|
|
270
|
+
capture(memoryCtx(CAROL_AT_CONSOLE), undefined),
|
|
271
|
+
);
|
|
272
|
+
expect(refusal.rawMessage).toBe(memoryDisabledMessage(ORG));
|
|
273
|
+
});
|
|
274
|
+
});
|
|
@@ -30,6 +30,17 @@ export function memoryDisabledMessage(org: string): string {
|
|
|
30
30
|
return `memory is not enabled for organization ${org} — an organization admin can enable it in organization preferences`;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Refuses a create while the PERSON the memory would be about has not
|
|
35
|
+
* enabled memory on their own account — the member half of the double
|
|
36
|
+
* opt-in (DD-006 D1), and the answer for a caller no account stands for,
|
|
37
|
+
* since only an unprovisioned person reaches that arm. The Java
|
|
38
|
+
* MemoryPolicy.MEMORY_ACCOUNT_DISABLED_MESSAGE, byte-pinned: the hosted
|
|
39
|
+
* edition's clients have rendered it since the Java handler shipped.
|
|
40
|
+
*/
|
|
41
|
+
export const MEMORY_ACCOUNT_DISABLED_MESSAGE =
|
|
42
|
+
"memory is not enabled for your account — enable it in account preferences";
|
|
43
|
+
|
|
33
44
|
/**
|
|
34
45
|
* Refuses confirming a rejected memory: the decision is auditable and
|
|
35
46
|
* stands; a fresh proposal is the way back. Go
|
|
@@ -35,12 +35,15 @@
|
|
|
35
35
|
*
|
|
36
36
|
* Authorization posture (OSS): this edition is single-user and local, so
|
|
37
37
|
* handlers perform no authorization — a documented no-op, not a silent
|
|
38
|
-
* divergence. The subject
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* both memory_enabled flags
|
|
43
|
-
*
|
|
38
|
+
* divergence. The subject and the double opt-in follow the POSTURE, not
|
|
39
|
+
* the edition (stigmer#1387): where callers are persons (the
|
|
40
|
+
* require-authentication posture composes `personAccounts` — the hosted
|
|
41
|
+
* edition, a self-host with sign-in) the subject is the identity account
|
|
42
|
+
* the memory is about and both memory_enabled flags must be on; under the
|
|
43
|
+
* single-operator posture (trusted-local) the subject is the empty-string
|
|
44
|
+
* sentinel (the OAuth grant store convention) and enablement is the org
|
|
45
|
+
* flag alone (the user scope collapses — DD-002 D1). Every RPC's FGA
|
|
46
|
+
* question is subject-only (can_view/can_edit/can_delete).
|
|
44
47
|
*
|
|
45
48
|
* Memory is deliberately NOT search-indexed (privacy — subject-only
|
|
46
49
|
* content must not surface in org-visible search): no search-extractor,
|
|
@@ -70,6 +73,7 @@ import type { Logger } from "../../boot/logger.js";
|
|
|
70
73
|
import type { Authorizer } from "../../extensions/authorizer.js";
|
|
71
74
|
import type { RunnerCredentialProvider } from "../../runnerauth/runner-credential-provider.js";
|
|
72
75
|
import type { ListReadScope } from "../../extensions/list-read-scope.js";
|
|
76
|
+
import type { AccountsByCaller } from "../identityaccount/resolve.js";
|
|
73
77
|
import type { ResourceAuthorizationLifecycle } from "../../extensions/resource-authorization.js";
|
|
74
78
|
import { apiResourceKindKey } from "../../pipeline/interceptors/apiresource.js";
|
|
75
79
|
import { internalError } from "../../pipeline/errors.js";
|
|
@@ -131,6 +135,14 @@ export interface MemoryControllerDeps {
|
|
|
131
135
|
readonly authorizationLifecycle: ResourceAuthorizationLifecycle | undefined;
|
|
132
136
|
/** The composed list read scope — list narrows through it; undefined = the OSS full scan (20260830.01). */
|
|
133
137
|
readonly listReadScope: ListReadScope | undefined;
|
|
138
|
+
/**
|
|
139
|
+
* The directory of the persons callers stand for (stigmer#1387) — the
|
|
140
|
+
* composed identity-account port, bound under the require-authentication
|
|
141
|
+
* posture; undefined = the single-operator posture (trusted-local),
|
|
142
|
+
* where memory is the organization's switch alone and the subject the
|
|
143
|
+
* empty-string sentinel.
|
|
144
|
+
*/
|
|
145
|
+
readonly personAccounts: AccountsByCaller | undefined;
|
|
134
146
|
/**
|
|
135
147
|
* The composed runner-credential provider — GuardMemoryCapture consults
|
|
136
148
|
* its authorizeMemoryCapture capability for the runner-credential
|
|
@@ -172,10 +184,13 @@ function kindOf(ctx: HandlerContext): ApiResourceKind {
|
|
|
172
184
|
* No search-index step: memory is not_search_indexed by design (privacy —
|
|
173
185
|
* content is subject-only and must not surface in org-visible search).
|
|
174
186
|
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
187
|
+
* Authorization: GuardMemoryCapture decides WHO may capture (the composed
|
|
188
|
+
* RunnerCredentialProvider), AuthorizeResolvedTarget decides WHERE
|
|
189
|
+
* (can_create_session on the organization), and CreateAuthorizationTuples
|
|
190
|
+
* runs against the composed lifecycle. ResolveMemoryDefaults names the
|
|
191
|
+
* person the memory is about and CheckMemoryEnablement requires both
|
|
192
|
+
* halves of the double opt-in where callers are persons (both step docs;
|
|
193
|
+
* stigmer#1387).
|
|
179
194
|
*/
|
|
180
195
|
async function createMemory(
|
|
181
196
|
deps: MemoryControllerDeps,
|
|
@@ -195,7 +210,7 @@ async function createMemory(
|
|
|
195
210
|
.addStep(newValidateProtoStep())
|
|
196
211
|
.addStep(newGuardMemoryCaptureStep(deps.runnerCredentialProvider))
|
|
197
212
|
.addStep(newValidateVisibilityStep())
|
|
198
|
-
.addStep(newResolveMemoryDefaultsStep())
|
|
213
|
+
.addStep(newResolveMemoryDefaultsStep(deps.personAccounts))
|
|
199
214
|
// Before the enablement and cap checks, so a refused caller learns
|
|
200
215
|
// nothing about the organization's settings (steps.ts,
|
|
201
216
|
// resolveMemoryCreateTargets).
|
|
@@ -205,7 +220,7 @@ async function createMemory(
|
|
|
205
220
|
resolveMemoryCreateTargets,
|
|
206
221
|
),
|
|
207
222
|
)
|
|
208
|
-
.addStep(newCheckMemoryEnablementStep(deps.store))
|
|
223
|
+
.addStep(newCheckMemoryEnablementStep(deps.store, deps.personAccounts))
|
|
209
224
|
.addStep(newCheckMemoryCapStep(deps.store))
|
|
210
225
|
.addStep(newResolveSlugStep())
|
|
211
226
|
.addStep(newCheckDuplicateStep(deps.store))
|
|
@@ -236,8 +251,7 @@ async function createMemory(
|
|
|
236
251
|
* owners (create, confirm, reject) last wrote it, even against a
|
|
237
252
|
* concurrent confirm landing between this pipeline's load and persist.
|
|
238
253
|
*
|
|
239
|
-
*
|
|
240
|
-
* requires can_edit on the memory — FGA subject-only).
|
|
254
|
+
* Authorize evaluates can_edit on the memory (subject-only in the model).
|
|
241
255
|
*/
|
|
242
256
|
async function update(
|
|
243
257
|
deps: MemoryControllerDeps,
|
|
@@ -332,9 +346,8 @@ async function reject(
|
|
|
332
346
|
* runTransition: one contract with opposite verdicts (DD-005 D3),
|
|
333
347
|
* answering with the post-image row.
|
|
334
348
|
*
|
|
335
|
-
*
|
|
336
|
-
*
|
|
337
|
-
* authorizing so a missing memory answers NOT_FOUND, #224).
|
|
349
|
+
* Authorize evaluates can_edit on the memory (subject-only in the model)
|
|
350
|
+
* against the request's id, before the load.
|
|
338
351
|
*/
|
|
339
352
|
async function runTransition(
|
|
340
353
|
deps: MemoryControllerDeps,
|
|
@@ -381,8 +394,7 @@ async function runTransition(
|
|
|
381
394
|
*
|
|
382
395
|
* No search-index cleanup: memory is not_search_indexed.
|
|
383
396
|
*
|
|
384
|
-
*
|
|
385
|
-
* requires can_delete on the memory — FGA subject-only).
|
|
397
|
+
* Authorize evaluates can_delete on the memory (subject-only in the model).
|
|
386
398
|
*
|
|
387
399
|
* The deleted memory is returned for audit trail purposes (gRPC
|
|
388
400
|
* convention).
|
|
@@ -425,9 +437,8 @@ async function deleteMemory(
|
|
|
425
437
|
/**
|
|
426
438
|
* Get — chain per Go buildGetPipeline (ExtractResourceId → LoadTarget).
|
|
427
439
|
*
|
|
428
|
-
*
|
|
429
|
-
*
|
|
430
|
-
* can read it).
|
|
440
|
+
* Authorize evaluates can_view on the memory: in the model only the person
|
|
441
|
+
* a memory is about can read it.
|
|
431
442
|
*/
|
|
432
443
|
async function get(
|
|
433
444
|
deps: MemoryControllerDeps,
|
|
@@ -466,11 +477,9 @@ async function get(
|
|
|
466
477
|
* console's presentation concern (DD-005 D4), deliberately not an RPC
|
|
467
478
|
* parameter at the kind's dozens-of-records scale.
|
|
468
479
|
*
|
|
469
|
-
*
|
|
470
|
-
*
|
|
471
|
-
*
|
|
472
|
-
* subject)
|
|
473
|
-
* - Pagination (returns all matching results)
|
|
480
|
+
* Narrowed to the caller's authorized rows (can_view, which resolves to
|
|
481
|
+
* the subject) by the composed list read scope. Unpaginated (returns all
|
|
482
|
+
* matching results).
|
|
474
483
|
*/
|
|
475
484
|
async function list(
|
|
476
485
|
deps: MemoryControllerDeps,
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* Proven by memory.conformance.test.ts (CONFORMANCE_TARGET=local) and
|
|
18
18
|
* __tests__/memory.test.ts.
|
|
19
19
|
*/
|
|
20
|
-
import { create, equals, fromBinary } from "@bufbuild/protobuf";
|
|
20
|
+
import { create, equals, fromBinary, isMessage } from "@bufbuild/protobuf";
|
|
21
21
|
import { timestampNow } from "@bufbuild/protobuf/wkt";
|
|
22
22
|
import type { ConnectError } from "@connectrpc/connect";
|
|
23
23
|
|
|
@@ -30,6 +30,8 @@ import { MemoryListSchema } from "@stigmer/protos/ai/stigmer/agentic/memory/v1/i
|
|
|
30
30
|
import { MemoryProvenanceSchema } from "@stigmer/protos/ai/stigmer/agentic/memory/v1/spec_pb";
|
|
31
31
|
import { MemoryStatusSchema } from "@stigmer/protos/ai/stigmer/agentic/memory/v1/status_pb";
|
|
32
32
|
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
33
|
+
import type { IdentityAccount } from "@stigmer/protos/ai/stigmer/iam/identityaccount/v1/api_pb";
|
|
34
|
+
import { IdentityAccountSchema } from "@stigmer/protos/ai/stigmer/iam/identityaccount/v1/api_pb";
|
|
33
35
|
import { IamPermission } from "@stigmer/protos/ai/stigmer/iam/v1/enum_pb";
|
|
34
36
|
import { OrganizationSchema } from "@stigmer/protos/ai/stigmer/tenancy/organization/v1/api_pb";
|
|
35
37
|
|
|
@@ -52,10 +54,16 @@ import {
|
|
|
52
54
|
import { memoryCaptureCredentialOf } from "../../pipeline/steps/guard-memory-capture.js";
|
|
53
55
|
import { compareCreatedAtDesc } from "../../pipeline/steps/helpers.js";
|
|
54
56
|
import { EXISTING_RESOURCE_KEY } from "../../pipeline/steps/load-existing.js";
|
|
57
|
+
import type { AccountsByCaller } from "../identityaccount/resolve.js";
|
|
58
|
+
import {
|
|
59
|
+
accountForCaller,
|
|
60
|
+
accountForStamp,
|
|
61
|
+
} from "../identityaccount/resolve.js";
|
|
55
62
|
import { ResourceNotFoundError } from "../../store/interface.js";
|
|
56
63
|
import type { Store } from "../../store/interface.js";
|
|
57
64
|
import {
|
|
58
65
|
MAX_MEMORIES_PER_SUBJECT,
|
|
66
|
+
MEMORY_ACCOUNT_DISABLED_MESSAGE,
|
|
59
67
|
MEMORY_FULL_MESSAGE,
|
|
60
68
|
MEMORY_PROVENANCE_IMMUTABLE_MESSAGE,
|
|
61
69
|
MEMORY_SUBJECT_IMMUTABLE_MESSAGE,
|
|
@@ -77,10 +85,22 @@ export const LIST_RESULT_KEY = "listResult";
|
|
|
77
85
|
* memories are id-addressed records (the remember tool sends content
|
|
78
86
|
* only), and the platform's slug machinery requires a name. A
|
|
79
87
|
* client-supplied name still wins — the default only fills absence.
|
|
80
|
-
* 3. Overwrites spec.subject_identity_account_id
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
88
|
+
* 3. Overwrites spec.subject_identity_account_id, server-derived and
|
|
89
|
+
* never client-supplied (DD-005 D2), by posture. Under the
|
|
90
|
+
* single-operator posture (no `personAccounts` composed) it is the
|
|
91
|
+
* empty-string sentinel — the laptop's one subject (the OAuth grant
|
|
92
|
+
* store convention). Where callers are persons it is the id of the
|
|
93
|
+
* identity account the memory is ABOUT (stigmer#1387, the Java
|
|
94
|
+
* MemoryCreateHandler's subject): the admitted capture credential's
|
|
95
|
+
* subject (point 5), else the calling person — each resolved through
|
|
96
|
+
* the identity-account domain (accountForStamp / accountForCaller),
|
|
97
|
+
* so a subject that arrived as a raw issuer subject is filed under
|
|
98
|
+
* the account every FGA tuple names. The resolved account rides the
|
|
99
|
+
* context under MEMORY_SUBJECT_ACCOUNT_KEY for CheckMemoryEnablement,
|
|
100
|
+
* so the subject stamped and the consent checked are one row, read
|
|
101
|
+
* once. A caller no account stands for is stamped "" and NOT refused
|
|
102
|
+
* here: the refusal belongs behind AuthorizeResolvedTarget, which
|
|
103
|
+
* runs between the two steps so a refused caller learns nothing.
|
|
84
104
|
* 4. Stores spec.provenance as supplied (Stage 3 provenance decision,
|
|
85
105
|
* owner-ratified 2026-08-22): the capture path — the remember tool
|
|
86
106
|
* via the runner-synthesized attachment — threads the agent/session/
|
|
@@ -101,15 +121,15 @@ export const LIST_RESULT_KEY = "listResult";
|
|
|
101
121
|
* human ("the sub IS the human subject the session belongs to",
|
|
102
122
|
* Java MemoryCreateHandler) and provenance.session_id is overridden
|
|
103
123
|
* with the token's own session claim (server-proved beats
|
|
104
|
-
* runner-reported). Absent the handoff, the
|
|
124
|
+
* runner-reported). Absent the handoff, the arms above apply
|
|
105
125
|
* unchanged.
|
|
106
126
|
*/
|
|
107
|
-
export function newResolveMemoryDefaultsStep(
|
|
108
|
-
|
|
109
|
-
> {
|
|
127
|
+
export function newResolveMemoryDefaultsStep(
|
|
128
|
+
personAccounts?: AccountsByCaller,
|
|
129
|
+
): PipelineStep<typeof MemorySchema> {
|
|
110
130
|
return {
|
|
111
131
|
name: "ResolveMemoryDefaults",
|
|
112
|
-
execute(ctx: RequestContext<typeof MemorySchema>): void {
|
|
132
|
+
async execute(ctx: RequestContext<typeof MemorySchema>): Promise<void> {
|
|
113
133
|
const memory = ctx.newState;
|
|
114
134
|
const metadata = memory.metadata;
|
|
115
135
|
|
|
@@ -138,11 +158,23 @@ export function newResolveMemoryDefaultsStep(): PipelineStep<
|
|
|
138
158
|
);
|
|
139
159
|
}
|
|
140
160
|
|
|
141
|
-
// The subject stays server-owned (DD-005 D2)
|
|
142
|
-
//
|
|
161
|
+
// The subject stays server-owned (DD-005 D2), by posture (step doc
|
|
162
|
+
// points 3 and 5).
|
|
143
163
|
const captureCredential = memoryCaptureCredentialOf(ctx);
|
|
144
|
-
|
|
145
|
-
|
|
164
|
+
if (personAccounts === undefined) {
|
|
165
|
+
spec.subjectIdentityAccountId =
|
|
166
|
+
captureCredential?.subjectIdentityAccountId ?? "";
|
|
167
|
+
} else {
|
|
168
|
+
const account = await resolveSubjectAccount(
|
|
169
|
+
personAccounts,
|
|
170
|
+
ctx,
|
|
171
|
+
captureCredential?.subjectIdentityAccountId,
|
|
172
|
+
);
|
|
173
|
+
spec.subjectIdentityAccountId = account?.metadata?.id ?? "";
|
|
174
|
+
if (account !== undefined) {
|
|
175
|
+
ctx.set(MEMORY_SUBJECT_ACCOUNT_KEY, account);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
146
178
|
|
|
147
179
|
// Provenance is capture-path-supplied (see the step doc, point 4);
|
|
148
180
|
// only tool_call_id is force-cleared — unreachable via MCP in v1, so
|
|
@@ -162,6 +194,54 @@ export function newResolveMemoryDefaultsStep(): PipelineStep<
|
|
|
162
194
|
};
|
|
163
195
|
}
|
|
164
196
|
|
|
197
|
+
/**
|
|
198
|
+
* Context key carrying the identity account a memory is about, from
|
|
199
|
+
* ResolveMemoryDefaults to CheckMemoryEnablement — present exactly when
|
|
200
|
+
* callers are persons and the subject resolved (step doc point 3).
|
|
201
|
+
*/
|
|
202
|
+
export const MEMORY_SUBJECT_ACCOUNT_KEY = "memorySubjectAccount";
|
|
203
|
+
|
|
204
|
+
/** The subject's account stashed by ResolveMemoryDefaults, if any. */
|
|
205
|
+
export function memorySubjectAccountOf(
|
|
206
|
+
ctx: RequestContext<typeof MemorySchema>,
|
|
207
|
+
): IdentityAccount | undefined {
|
|
208
|
+
const payload = ctx.get(MEMORY_SUBJECT_ACCOUNT_KEY);
|
|
209
|
+
return isMessage(payload, IdentityAccountSchema) ? payload : undefined;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* The account the memory is about: the capture credential's subject read
|
|
214
|
+
* as a creator-style stamp (an account id, or a raw issuer subject), else
|
|
215
|
+
* the account the calling person stands for. `undefined` when nobody
|
|
216
|
+
* resolves. A store fault is an infrastructure fault; an account row with
|
|
217
|
+
* no id is a loud one, never an empty principal (the resolve.ts rule).
|
|
218
|
+
*/
|
|
219
|
+
async function resolveSubjectAccount(
|
|
220
|
+
personAccounts: AccountsByCaller,
|
|
221
|
+
ctx: RequestContext<typeof MemorySchema>,
|
|
222
|
+
credentialSubject: string | undefined,
|
|
223
|
+
): Promise<IdentityAccount | undefined> {
|
|
224
|
+
let account: IdentityAccount | undefined;
|
|
225
|
+
try {
|
|
226
|
+
account =
|
|
227
|
+
credentialSubject !== undefined
|
|
228
|
+
? await accountForStamp(personAccounts, credentialSubject)
|
|
229
|
+
: await accountForCaller(personAccounts, ctx.callerIdentity);
|
|
230
|
+
} catch (error) {
|
|
231
|
+
throw internalError(
|
|
232
|
+
error,
|
|
233
|
+
"failed to resolve the identity account a memory is about",
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
if (account !== undefined && (account.metadata?.id ?? "") === "") {
|
|
237
|
+
throw internalError(
|
|
238
|
+
new Error("identity account carries no id"),
|
|
239
|
+
"failed to resolve the identity account a memory is about",
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
return account;
|
|
243
|
+
}
|
|
244
|
+
|
|
165
245
|
/** The deny copy of the create lane's organization bar. */
|
|
166
246
|
export const MEMORY_CREATE_DENIED_MESSAGE =
|
|
167
247
|
"unauthorized to capture memory in this organization";
|
|
@@ -212,15 +292,22 @@ export function resolveMemoryCreateTargets(
|
|
|
212
292
|
* authorization: "the label is not authorization; the server refuses"
|
|
213
293
|
* (the conversation-attachment doctrine, applied verbatim).
|
|
214
294
|
*
|
|
215
|
-
*
|
|
216
|
-
*
|
|
217
|
-
*
|
|
295
|
+
* The double opt-in (DD-006 D1), in the Java handler's order: the
|
|
296
|
+
* organization's switch first (the gate an admin controls), then — where
|
|
297
|
+
* callers are persons (`personAccounts` composed) — the switch of the
|
|
298
|
+
* person the memory is about, read from the account ResolveMemoryDefaults
|
|
299
|
+
* resolved and stashed (stigmer#1387). No stashed account under that
|
|
300
|
+
* posture refuses with the person's copy: nobody the server can name has
|
|
301
|
+
* consented. The single-operator posture checks the org flag alone — its
|
|
302
|
+
* one operator is the organization's decider. Who may capture at all is
|
|
303
|
+
* GuardMemoryCapture's question, not this step's.
|
|
218
304
|
*
|
|
219
305
|
* Reads the Organization row directly from the store, matching Go — no
|
|
220
306
|
* cross-domain client.
|
|
221
307
|
*/
|
|
222
308
|
export function newCheckMemoryEnablementStep(
|
|
223
309
|
store: Store,
|
|
310
|
+
personAccounts?: AccountsByCaller,
|
|
224
311
|
): PipelineStep<typeof MemorySchema> {
|
|
225
312
|
return {
|
|
226
313
|
name: "CheckMemoryEnablement",
|
|
@@ -247,6 +334,14 @@ export function newCheckMemoryEnablementStep(
|
|
|
247
334
|
if (org.spec?.preferences?.memoryEnabled !== true) {
|
|
248
335
|
throw failedPreconditionError(memoryDisabledMessage(orgID));
|
|
249
336
|
}
|
|
337
|
+
|
|
338
|
+
if (personAccounts === undefined) {
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
const subject = memorySubjectAccountOf(ctx);
|
|
342
|
+
if (subject?.spec?.preferences?.memoryEnabled !== true) {
|
|
343
|
+
throw failedPreconditionError(MEMORY_ACCOUNT_DISABLED_MESSAGE);
|
|
344
|
+
}
|
|
250
345
|
},
|
|
251
346
|
};
|
|
252
347
|
}
|
|
@@ -20,10 +20,13 @@
|
|
|
20
20
|
* carries the other two edition forks: find's enumeration posture and
|
|
21
21
|
* findMyOrganizations' filtering (see organization-directory.ts).
|
|
22
22
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
23
|
+
* Every chain opens with Authorize; create and delete run the shared
|
|
24
|
+
* tuple-lifecycle steps (CreateAuthorizationTuples, CleanupIamPolicies)
|
|
25
|
+
* against the lifecycle compose.ts hands it: a composed
|
|
26
|
+
* resourceAuthorizationLifecycle driver when a unit registers one, else,
|
|
27
|
+
* under the built-in posture, open source's role lifecycle (the creator's
|
|
28
|
+
* `owner` row), and under trusted-local none, where the steps no-op.
|
|
29
|
+
* Per-RPC posture: docs/authorization-coverage.md §2.
|
|
27
30
|
*/
|
|
28
31
|
import type { ConnectRouter, HandlerContext } from "@connectrpc/connect";
|
|
29
32
|
import { Code, ConnectError } from "@connectrpc/connect";
|