@stigmer/server 3.32.0 → 3.33.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/dist/boot/compose.d.ts.map +1 -1
- package/dist/boot/compose.js +8 -1
- package/dist/boot/compose.js.map +1 -1
- package/dist/domain/apikey/controller.d.ts.map +1 -1
- package/dist/domain/apikey/controller.js +7 -2
- package/dist/domain/apikey/controller.js.map +1 -1
- package/dist/domain/apikey/verifier.d.ts +5 -0
- package/dist/domain/apikey/verifier.d.ts.map +1 -1
- package/dist/domain/apikey/verifier.js +32 -3
- package/dist/domain/apikey/verifier.js.map +1 -1
- package/dist/domain/platformclient/mint.d.ts.map +1 -1
- package/dist/domain/platformclient/mint.js +28 -1
- package/dist/domain/platformclient/mint.js.map +1 -1
- package/dist/domain/platformclient/resource-store.d.ts.map +1 -1
- package/dist/domain/platformclient/resource-store.js +13 -0
- package/dist/domain/platformclient/resource-store.js.map +1 -1
- package/dist/domain/platformclient/store-contract.d.ts.map +1 -1
- package/dist/domain/platformclient/store-contract.js +59 -0
- package/dist/domain/platformclient/store-contract.js.map +1 -1
- package/dist/domain/platformclient/store.d.ts +20 -0
- package/dist/domain/platformclient/store.d.ts.map +1 -1
- package/dist/domain/platformclient/store.js.map +1 -1
- package/dist/identity/credential-use.d.ts +74 -0
- package/dist/identity/credential-use.d.ts.map +1 -0
- package/dist/identity/credential-use.js +59 -0
- package/dist/identity/credential-use.js.map +1 -0
- package/dist/pipeline/steps/defaults.d.ts +10 -0
- package/dist/pipeline/steps/defaults.d.ts.map +1 -1
- package/dist/pipeline/steps/defaults.js +25 -0
- package/dist/pipeline/steps/defaults.js.map +1 -1
- package/dist/temporal/schedule/activities.d.ts.map +1 -1
- package/dist/temporal/schedule/activities.js +2 -1
- package/dist/temporal/schedule/activities.js.map +1 -1
- package/dist/temporal/schedule/run-starter.d.ts.map +1 -1
- package/dist/temporal/schedule/run-starter.js +2 -1
- package/dist/temporal/schedule/run-starter.js.map +1 -1
- package/dist/temporal/schedule/status-writes.d.ts +0 -6
- package/dist/temporal/schedule/status-writes.d.ts.map +1 -1
- package/dist/temporal/schedule/status-writes.js +8 -27
- package/dist/temporal/schedule/status-writes.js.map +1 -1
- package/dist/temporal/schedule/syncer.d.ts.map +1 -1
- package/dist/temporal/schedule/syncer.js +2 -1
- package/dist/temporal/schedule/syncer.js.map +1 -1
- package/package.json +6 -6
- package/src/boot/compose.ts +8 -1
- package/src/domain/apikey/__tests__/apikey.test.ts +17 -2
- package/src/domain/apikey/__tests__/verifier.test.ts +86 -3
- package/src/domain/apikey/controller.ts +12 -2
- package/src/domain/apikey/verifier.ts +46 -3
- package/src/domain/platformclient/__tests__/mint.test.ts +97 -11
- package/src/domain/platformclient/__tests__/resource-store.test.ts +4 -0
- package/src/domain/platformclient/mint.ts +34 -1
- package/src/domain/platformclient/resource-store.ts +18 -0
- package/src/domain/platformclient/store-contract.ts +100 -0
- package/src/domain/platformclient/store.ts +23 -0
- package/src/identity/__tests__/credential-use.test.ts +186 -0
- package/src/identity/credential-use.ts +130 -0
- package/src/pipeline/steps/defaults.ts +27 -0
- package/src/temporal/schedule/activities.ts +2 -1
- package/src/temporal/schedule/run-starter.ts +2 -1
- package/src/temporal/schedule/status-writes.ts +8 -31
- package/src/temporal/schedule/syncer.ts +2 -1
|
@@ -1,20 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The clock's status-write
|
|
3
|
-
*
|
|
2
|
+
* The clock's status-write helper — ports ensureStatus of
|
|
3
|
+
* pkg/domain/schedule/temporal/syncer.go.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* steps.SetAuditFieldsForUpdate from the clock's bump. Two flavors, two
|
|
12
|
-
* writers, both wire-visible — keep them distinct.
|
|
5
|
+
* The clock's audit stamp is the shared `bumpStatusAudit`
|
|
6
|
+
* (pipeline/steps/defaults.ts): status-audit updated_at + event only, never
|
|
7
|
+
* an actor — the clock is the platform, not an operator. Controller-side
|
|
8
|
+
* status writes (trigger's last_fire_at stamp, resume's clear) use the FULL
|
|
9
|
+
* setAuditFieldsForUpdate instead, exactly as Go splits
|
|
10
|
+
* steps.SetAuditFieldsForUpdate from the clock's bump.
|
|
13
11
|
*/
|
|
14
12
|
import { create } from "@bufbuild/protobuf";
|
|
15
|
-
import { timestampNow } from "@bufbuild/protobuf/wkt";
|
|
16
13
|
import { ScheduleStatusSchema } from "@stigmer/protos/ai/stigmer/agentic/schedule/v1/status_pb";
|
|
17
|
-
import { ApiResourceAuditInfoSchema, ApiResourceAuditSchema, } from "@stigmer/protos/ai/stigmer/commons/apiresource/status_pb";
|
|
18
14
|
/**
|
|
19
15
|
* Returns the schedule's status, materializing the nested message a fresh
|
|
20
16
|
* row may lack (Go ensureStatus).
|
|
@@ -25,19 +21,4 @@ export function ensureStatus(schedule) {
|
|
|
25
21
|
}
|
|
26
22
|
return schedule.status;
|
|
27
23
|
}
|
|
28
|
-
/**
|
|
29
|
-
* Stamps the status-audit trail for a runtime write — updated_at + event
|
|
30
|
-
* "updated", nothing else (Go bumpStatusAudit; see the module header for
|
|
31
|
-
* why this is not setAuditFieldsForUpdate).
|
|
32
|
-
*/
|
|
33
|
-
export function bumpStatusAudit(status) {
|
|
34
|
-
if (status.audit === undefined) {
|
|
35
|
-
status.audit = create(ApiResourceAuditSchema);
|
|
36
|
-
}
|
|
37
|
-
if (status.audit.statusAudit === undefined) {
|
|
38
|
-
status.audit.statusAudit = create(ApiResourceAuditInfoSchema);
|
|
39
|
-
}
|
|
40
|
-
status.audit.statusAudit.updatedAt = timestampNow();
|
|
41
|
-
status.audit.statusAudit.event = "updated";
|
|
42
|
-
}
|
|
43
24
|
//# sourceMappingURL=status-writes.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"status-writes.js","sourceRoot":"","sources":["../../../src/temporal/schedule/status-writes.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"status-writes.js","sourceRoot":"","sources":["../../../src/temporal/schedule/status-writes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAG5C,OAAO,EAAE,oBAAoB,EAAE,MAAM,0DAA0D,CAAC;AAGhG;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,QAAkB;IAC7C,IAAI,QAAQ,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;QAClC,QAAQ,CAAC,MAAM,GAAG,MAAM,CAAC,oBAAoB,CAAC,CAAC;IACjD,CAAC;IACD,OAAO,QAAQ,CAAC,MAAM,CAAC;AACzB,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"syncer.d.ts","sourceRoot":"","sources":["../../../src/temporal/schedule/syncer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,OAAO,KAAK,EAAE,MAAM,EAAuB,MAAM,oBAAoB,CAAC;AAKtE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,uDAAuD,CAAC;AAGtF,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"syncer.d.ts","sourceRoot":"","sources":["../../../src/temporal/schedule/syncer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,OAAO,KAAK,EAAE,MAAM,EAAuB,MAAM,oBAAoB,CAAC;AAKtE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,uDAAuD,CAAC;AAGtF,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAGnD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,0BAA0B,CAAC;AACtD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAKtD;;;;GAIG;AACH,qBAAa,wBAAyB,SAAQ,KAAK;;CAKlD;AAED,qBAAa,cAAc;IAEvB,+DAA+D;IAC/D,OAAO,CAAC,QAAQ,CAAC,cAAc;IAC/B,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,MAAM;;IAJvB,+DAA+D;IAC9C,cAAc,EAAE,MAAM,MAAM,GAAG,SAAS,EACxC,KAAK,EAAE,KAAK,EACZ,QAAQ,EAAE,gBAAgB,EAC1B,MAAM,EAAE,MAAM;IAGjC;;;;;OAKG;IACG,eAAe,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,GAAG,SAAS,CAAC;IA+CpE;;;;;OAKG;IACG,cAAc,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,GAAG,SAAS,CAAC;IAqBnE;;;;OAIG;IACG,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAoBjD;;;;OAIG;YACW,gBAAgB;CA6B/B"}
|
|
@@ -2,10 +2,11 @@ import { ScheduleAlreadyRunning, ScheduleNotFoundError } from "@temporalio/clien
|
|
|
2
2
|
import { timestampFromDate } from "@bufbuild/protobuf/wkt";
|
|
3
3
|
import { ScheduleSchema } from "@stigmer/protos/ai/stigmer/agentic/schedule/v1/api_pb";
|
|
4
4
|
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
5
|
+
import { bumpStatusAudit } from "../../pipeline/steps/defaults.js";
|
|
5
6
|
import { ResourceNotFoundError } from "../../store/interface.js";
|
|
6
7
|
import { desiredPaused, note } from "./artifact.js";
|
|
7
8
|
import { artifactId } from "./names.js";
|
|
8
|
-
import {
|
|
9
|
+
import { ensureStatus } from "./status-writes.js";
|
|
9
10
|
/**
|
|
10
11
|
* The syncer's answer when no Temporal client exists right now (Go
|
|
11
12
|
* ErrTemporalUnavailable). Callers on the write path treat it as "converge
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"syncer.js","sourceRoot":"","sources":["../../../src/temporal/schedule/syncer.ts"],"names":[],"mappings":"AA4BA,OAAO,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AACnF,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAE3D,OAAO,EAAE,cAAc,EAAE,MAAM,uDAAuD,CAAC;AAEvF,OAAO,EAAE,eAAe,EAAE,MAAM,qFAAqF,CAAC;AAGtH,OAAO,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AAGjE,OAAO,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,eAAe,CAAC;AACpD,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACxC,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"syncer.js","sourceRoot":"","sources":["../../../src/temporal/schedule/syncer.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stigmer/server",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.33.0",
|
|
4
4
|
"description": "The Stigmer OSS control plane as a composable library: composeServer, the extension registry and the driver seams a composition builds on. The self-contained deployable of the same server is @stigmer/server-slim.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -63,11 +63,11 @@
|
|
|
63
63
|
"@connectrpc/connect-node": "^2.0.0",
|
|
64
64
|
"@kubernetes/client-node": "2.0.0",
|
|
65
65
|
"@opentelemetry/api": "^1.9.0",
|
|
66
|
-
"@stigmer/outbound": "3.
|
|
67
|
-
"@stigmer/plugin-package": "3.
|
|
68
|
-
"@stigmer/protos": "3.
|
|
69
|
-
"@stigmer/temporal-codecs": "3.
|
|
70
|
-
"@stigmer/zip-structure": "3.
|
|
66
|
+
"@stigmer/outbound": "3.33.0",
|
|
67
|
+
"@stigmer/plugin-package": "3.33.0",
|
|
68
|
+
"@stigmer/protos": "3.33.0",
|
|
69
|
+
"@stigmer/temporal-codecs": "3.33.0",
|
|
70
|
+
"@stigmer/zip-structure": "3.33.0",
|
|
71
71
|
"@temporalio/activity": "1.16.2",
|
|
72
72
|
"@temporalio/client": "1.16.2",
|
|
73
73
|
"@temporalio/common": "1.16.2",
|
package/src/boot/compose.ts
CHANGED
|
@@ -1693,7 +1693,14 @@ export async function composeServer(
|
|
|
1693
1693
|
// own (the cloud's sandbox lanes).
|
|
1694
1694
|
const identityVerifiers = [
|
|
1695
1695
|
...(requireAuthentication
|
|
1696
|
-
? [
|
|
1696
|
+
? [
|
|
1697
|
+
newApiKeyIdentityVerifier({
|
|
1698
|
+
store,
|
|
1699
|
+
accounts: identityAccounts,
|
|
1700
|
+
logger,
|
|
1701
|
+
now: () => new Date(),
|
|
1702
|
+
}),
|
|
1703
|
+
]
|
|
1697
1704
|
: []),
|
|
1698
1705
|
// The PlatformClient user-token lane rides the ring, which the keys
|
|
1699
1706
|
// stage resolves only under an authentication posture. It sits before
|
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
* values; only the expiry fields move;
|
|
13
13
|
* - getByKeyHash resolves by the stored hash and answers the
|
|
14
14
|
* byte-pinned "ApiKey not found" for an unknown or deleted hash —
|
|
15
|
-
* deletion takes effect on the very next lookup (no cache)
|
|
15
|
+
* deletion takes effect on the very next lookup (no cache);
|
|
16
|
+
* - findAll answers newest first, so a key's last-use stamp rewriting
|
|
17
|
+
* its row never reorders the list (stigmer/stigmer#1255).
|
|
16
18
|
*/
|
|
17
19
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
18
20
|
import { tmpdir } from "node:os";
|
|
@@ -21,7 +23,7 @@ import path from "node:path";
|
|
|
21
23
|
import { Code, ConnectError, createClient } from "@connectrpc/connect";
|
|
22
24
|
import type { Client, Transport } from "@connectrpc/connect";
|
|
23
25
|
import { createGrpcTransport } from "@connectrpc/connect-node";
|
|
24
|
-
import { timestampFromDate } from "@bufbuild/protobuf/wkt";
|
|
26
|
+
import { timestampDate, timestampFromDate } from "@bufbuild/protobuf/wkt";
|
|
25
27
|
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
26
28
|
|
|
27
29
|
import { ApiKeyCommandController } from "@stigmer/protos/ai/stigmer/iam/apikey/v1/command_pb";
|
|
@@ -250,4 +252,17 @@ describe("apikey domain (composed server)", () => {
|
|
|
250
252
|
expect(mine).toBeDefined();
|
|
251
253
|
expect(mine?.spec?.keyHash).toBe(hashApiKey(plaintext));
|
|
252
254
|
});
|
|
255
|
+
|
|
256
|
+
it("findAll answers newest first", async () => {
|
|
257
|
+
for (let i = 0; i < 3; i += 1) {
|
|
258
|
+
await command.create(keyInput());
|
|
259
|
+
}
|
|
260
|
+
const created = (await query.findAll({})).entries.map((entry) => {
|
|
261
|
+
const at = entry.status?.audit?.specAudit?.createdAt;
|
|
262
|
+
expect(at, "every stored key carries its creation instant").toBeDefined();
|
|
263
|
+
return timestampDate(at!).getTime();
|
|
264
|
+
});
|
|
265
|
+
expect(created.length).toBeGreaterThanOrEqual(3);
|
|
266
|
+
expect(created).toEqual([...created].sort((a, b) => b - a));
|
|
267
|
+
});
|
|
253
268
|
});
|
|
@@ -15,13 +15,18 @@
|
|
|
15
15
|
* the REAL open-source adapter over the same SQLite store the keys live
|
|
16
16
|
* in, so what is pinned is the production pair; only the fault arm
|
|
17
17
|
* substitutes a one-method lookup that rejects.
|
|
18
|
+
*
|
|
19
|
+
* An accepted key's use lands on its own row (stigmer/stigmer#1255):
|
|
20
|
+
* stamped on the first verify, left alone inside the resolution, moved
|
|
21
|
+
* forward after it; a refused key records nothing; a stamp that cannot be
|
|
22
|
+
* written is logged and the request is still authenticated.
|
|
18
23
|
*/
|
|
19
24
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
20
25
|
import { tmpdir } from "node:os";
|
|
21
26
|
import path from "node:path";
|
|
22
27
|
|
|
23
28
|
import { create } from "@bufbuild/protobuf";
|
|
24
|
-
import { timestampFromDate } from "@bufbuild/protobuf/wkt";
|
|
29
|
+
import { timestampDate, timestampFromDate } from "@bufbuild/protobuf/wkt";
|
|
25
30
|
import { Code, ConnectError } from "@connectrpc/connect";
|
|
26
31
|
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
27
32
|
|
|
@@ -31,6 +36,9 @@ import { IdentityAccountSchema } from "@stigmer/protos/ai/stigmer/iam/identityac
|
|
|
31
36
|
import { IdentityAccountProvisioningMode } from "@stigmer/protos/ai/stigmer/iam/identityaccount/v1/enum_pb";
|
|
32
37
|
|
|
33
38
|
import { createLogger } from "../../../boot/logger.js";
|
|
39
|
+
import type { Logger } from "../../../boot/logger.js";
|
|
40
|
+
import { LAST_USED_RESOLUTION_MS } from "../../../identity/credential-use.js";
|
|
41
|
+
import type { Store } from "../../../store/interface.js";
|
|
34
42
|
import { SqliteStore } from "../../../store/sqlite/store.js";
|
|
35
43
|
import { accountIdFor } from "../../identityaccount/constants.js";
|
|
36
44
|
import type { AccountsBySubject } from "../../identityaccount/resolve.js";
|
|
@@ -56,9 +64,24 @@ beforeAll(() => {
|
|
|
56
64
|
accounts = newResourceIdentityAccountStore(store);
|
|
57
65
|
});
|
|
58
66
|
|
|
67
|
+
const silent = createLogger({ level: "error", pretty: false, write: () => {} });
|
|
68
|
+
|
|
59
69
|
/** The verifier as compose builds it: the key store and the account port over ONE store. */
|
|
60
|
-
function verifier(
|
|
61
|
-
|
|
70
|
+
function verifier(
|
|
71
|
+
lookup: AccountsBySubject = accounts,
|
|
72
|
+
options: { store?: Store; now?: () => Date; logger?: Logger } = {},
|
|
73
|
+
) {
|
|
74
|
+
return newApiKeyIdentityVerifier({
|
|
75
|
+
store: options.store ?? store,
|
|
76
|
+
accounts: lookup,
|
|
77
|
+
logger: options.logger ?? silent,
|
|
78
|
+
now: options.now ?? (() => new Date()),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** The key's row as stored now. */
|
|
83
|
+
function storedKey(id: string) {
|
|
84
|
+
return store.getResource(ApiResourceKind.api_key, id, ApiKeySchema);
|
|
62
85
|
}
|
|
63
86
|
|
|
64
87
|
/** A direct account for `sub` through the real adapter — derived id, as provisioning writes it. */
|
|
@@ -302,3 +325,63 @@ describe("creator stamp → account resolution (20260911.11 A6)", () => {
|
|
|
302
325
|
expect(reads).toEqual([]);
|
|
303
326
|
});
|
|
304
327
|
});
|
|
328
|
+
|
|
329
|
+
describe("last use (stigmer/stigmer#1255)", () => {
|
|
330
|
+
it("an accepted key stamps its last use on its own row, once per resolution, forward after it", async () => {
|
|
331
|
+
const { plaintext, id } = await seedKey();
|
|
332
|
+
let clock = new Date("2026-09-28T12:00:00Z");
|
|
333
|
+
const apikey = verifier(accounts, { now: () => clock });
|
|
334
|
+
|
|
335
|
+
expect((await storedKey(id)).status?.lastUsedAt).toBeUndefined();
|
|
336
|
+
await apikey.verify(plaintext);
|
|
337
|
+
const first = (await storedKey(id)).status;
|
|
338
|
+
expect(timestampDate(first!.lastUsedAt!)).toEqual(clock);
|
|
339
|
+
expect(first?.audit?.statusAudit?.event).toBe("updated");
|
|
340
|
+
expect(
|
|
341
|
+
first?.audit?.statusAudit?.createdBy?.id,
|
|
342
|
+
"the status-audit slot keeps who created the key",
|
|
343
|
+
).toBe("user@example.com");
|
|
344
|
+
expect(first?.audit?.specAudit?.updatedAt, "the spec audit is not touched").toBeUndefined();
|
|
345
|
+
|
|
346
|
+
clock = new Date(clock.getTime() + LAST_USED_RESOLUTION_MS - 1);
|
|
347
|
+
await apikey.verify(plaintext);
|
|
348
|
+
expect(
|
|
349
|
+
(await storedKey(id)).status,
|
|
350
|
+
"inside the resolution nothing is written",
|
|
351
|
+
).toEqual(first);
|
|
352
|
+
|
|
353
|
+
clock = new Date(clock.getTime() + 1);
|
|
354
|
+
await apikey.verify(plaintext);
|
|
355
|
+
expect(timestampDate((await storedKey(id)).status!.lastUsedAt!)).toEqual(clock);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it("a refused key records nothing", async () => {
|
|
359
|
+
const { plaintext, id } = await seedKey({
|
|
360
|
+
expiresAt: new Date(Date.now() - 60_000),
|
|
361
|
+
});
|
|
362
|
+
await expect(verifier().verify(plaintext)).rejects.toBeInstanceOf(ConnectError);
|
|
363
|
+
expect((await storedKey(id)).status?.lastUsedAt).toBeUndefined();
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it("a stamp that cannot be written is logged with the key's id and the request is still authenticated", async () => {
|
|
367
|
+
const { plaintext, id } = await seedKey({ ownerId: "ida_resilient" });
|
|
368
|
+
const lines: string[] = [];
|
|
369
|
+
const logger = createLogger({
|
|
370
|
+
level: "warn",
|
|
371
|
+
pretty: false,
|
|
372
|
+
write: (line) => lines.push(line),
|
|
373
|
+
});
|
|
374
|
+
const failingWrites = {
|
|
375
|
+
findByField: store.findByField.bind(store),
|
|
376
|
+
updateResource: () => Promise.reject(new Error("disk full")),
|
|
377
|
+
} as unknown as Store;
|
|
378
|
+
|
|
379
|
+
const identity = await verifier(accounts, { store: failingWrites, logger }).verify(plaintext);
|
|
380
|
+
|
|
381
|
+
expect(identity?.identityId).toBe("ida_resilient");
|
|
382
|
+
expect(lines).toHaveLength(1);
|
|
383
|
+
const entry = JSON.parse(lines[0]!) as Record<string, unknown>;
|
|
384
|
+
expect(entry).toMatchObject({ level: "warn", credential: "api key", id, error: "disk full" });
|
|
385
|
+
expect(lines[0], "the token is never logged").not.toContain(plaintext);
|
|
386
|
+
});
|
|
387
|
+
});
|
|
@@ -58,6 +58,7 @@ import {
|
|
|
58
58
|
newLoadExistingForDeleteStep,
|
|
59
59
|
} from "../../pipeline/steps/delete.js";
|
|
60
60
|
import { newCheckDuplicateStep } from "../../pipeline/steps/duplicate.js";
|
|
61
|
+
import { compareCreatedAtDesc } from "../../pipeline/steps/helpers.js";
|
|
61
62
|
import {
|
|
62
63
|
EXISTING_RESOURCE_KEY,
|
|
63
64
|
newLoadExistingStep,
|
|
@@ -289,7 +290,10 @@ async function getByKeyHash(
|
|
|
289
290
|
* 9) the list narrows to the caller's can_view keys — the Java
|
|
290
291
|
* ApiKeyFindAllHandler baseline (no guest arm, no org intersection).
|
|
291
292
|
* Stored hashes ride the response exactly as the cloud's do — the
|
|
292
|
-
* plaintext exists nowhere.
|
|
293
|
+
* plaintext exists nowhere. Newest first, as PlatformClient's listByOrg
|
|
294
|
+
* answers: the store's scan has no order, and a key's own last-use stamp
|
|
295
|
+
* rewrites its row, so an unsorted list would reorder as keys are used
|
|
296
|
+
* (stigmer/stigmer#1255).
|
|
293
297
|
*/
|
|
294
298
|
async function findAll(
|
|
295
299
|
deps: ApiKeyControllerDeps,
|
|
@@ -319,12 +323,18 @@ async function findAll(
|
|
|
319
323
|
} catch (error) {
|
|
320
324
|
throw internalError(error, "failed to list api keys");
|
|
321
325
|
}
|
|
322
|
-
const
|
|
326
|
+
const visible = await restrictListByReadScope(
|
|
323
327
|
deps.listReadScope,
|
|
324
328
|
identity,
|
|
325
329
|
ApiResourceKind.api_key,
|
|
326
330
|
rows.map((row) => fromBinary(ApiKeySchema, row)),
|
|
327
331
|
"",
|
|
328
332
|
);
|
|
333
|
+
const entries = [...visible].sort((a, b) =>
|
|
334
|
+
compareCreatedAtDesc(
|
|
335
|
+
a.status?.audit?.specAudit?.createdAt,
|
|
336
|
+
b.status?.audit?.specAudit?.createdAt,
|
|
337
|
+
),
|
|
338
|
+
);
|
|
329
339
|
return create(ApiKeysSchema, { entries });
|
|
330
340
|
}
|
|
@@ -33,14 +33,28 @@
|
|
|
33
33
|
* identityId changes on a hit: email/displayName stay the stamp's, the
|
|
34
34
|
* actor the key recorded at minting. The credential checks run first, so
|
|
35
35
|
* a revoked or expired key never reaches the account store.
|
|
36
|
+
*
|
|
37
|
+
* An accepted key's use is then recorded on its status.last_used_at
|
|
38
|
+
* (identity/credential-use.ts): at most once a resolution, judged from the
|
|
39
|
+
* row this read already holds, written atomically through
|
|
40
|
+
* Store.updateResource so it can never undo a concurrent update of the
|
|
41
|
+
* key, and best-effort — a failed stamp is logged and the request
|
|
42
|
+
* proceeds. A refused key records nothing (stigmer/stigmer#1255).
|
|
36
43
|
*/
|
|
37
44
|
import { Code, ConnectError } from "@connectrpc/connect";
|
|
45
|
+
import { create } from "@bufbuild/protobuf";
|
|
38
46
|
import { timestampDate } from "@bufbuild/protobuf/wkt";
|
|
39
47
|
|
|
48
|
+
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
49
|
+
import { ApiKeySchema, ApiKeyStatusSchema } from "@stigmer/protos/ai/stigmer/iam/apikey/v1/api_pb";
|
|
50
|
+
import type { ApiKey } from "@stigmer/protos/ai/stigmer/iam/apikey/v1/api_pb";
|
|
51
|
+
|
|
52
|
+
import type { Logger } from "../../boot/logger.js";
|
|
40
53
|
import type {
|
|
41
54
|
CallerIdentity,
|
|
42
55
|
IdentityVerifier,
|
|
43
56
|
} from "../../extensions/identity.js";
|
|
57
|
+
import { recordCredentialUse } from "../../identity/credential-use.js";
|
|
44
58
|
import type { Store } from "../../store/interface.js";
|
|
45
59
|
import { identityIdForSubject } from "../identityaccount/resolve.js";
|
|
46
60
|
import type { AccountsBySubject } from "../identityaccount/resolve.js";
|
|
@@ -58,12 +72,16 @@ export interface ApiKeyVerifierDeps {
|
|
|
58
72
|
readonly store: Store;
|
|
59
73
|
/** The identity-account domain's subject lookup the creator stamp resolves through (A6). */
|
|
60
74
|
readonly accounts: AccountsBySubject;
|
|
75
|
+
/** Where a failed last-use stamp is reported. */
|
|
76
|
+
readonly logger: Logger;
|
|
77
|
+
/** The clock both the expiry check and the last-use stamp read. */
|
|
78
|
+
readonly now: () => Date;
|
|
61
79
|
}
|
|
62
80
|
|
|
63
81
|
export function newApiKeyIdentityVerifier(
|
|
64
82
|
deps: ApiKeyVerifierDeps,
|
|
65
83
|
): IdentityVerifier {
|
|
66
|
-
const { store, accounts } = deps;
|
|
84
|
+
const { store, accounts, logger } = deps;
|
|
67
85
|
return {
|
|
68
86
|
name: "apikey",
|
|
69
87
|
async verify(token: string): Promise<CallerIdentity | null> {
|
|
@@ -74,8 +92,9 @@ export function newApiKeyIdentityVerifier(
|
|
|
74
92
|
if (key === undefined) {
|
|
75
93
|
throw new ConnectError(INVALID_TOKEN_MESSAGE, Code.Unauthenticated);
|
|
76
94
|
}
|
|
95
|
+
const now = deps.now();
|
|
77
96
|
const expiresAt = key.spec?.expiresAt;
|
|
78
|
-
if (expiresAt !== undefined && timestampDate(expiresAt) <=
|
|
97
|
+
if (expiresAt !== undefined && timestampDate(expiresAt) <= now) {
|
|
79
98
|
throw new ConnectError(TOKEN_EXPIRED_MESSAGE, Code.Unauthenticated);
|
|
80
99
|
}
|
|
81
100
|
const owner = key.status?.audit?.specAudit?.createdBy;
|
|
@@ -85,8 +104,10 @@ export function newApiKeyIdentityVerifier(
|
|
|
85
104
|
// through the pipeline; guards hand-seeded or corrupted rows).
|
|
86
105
|
throw new ConnectError(INVALID_TOKEN_MESSAGE, Code.Unauthenticated);
|
|
87
106
|
}
|
|
107
|
+
const identityId = await identityIdForSubject(accounts, owner.id);
|
|
108
|
+
await recordKeyUse(store, logger, key, now);
|
|
88
109
|
return {
|
|
89
|
-
identityId
|
|
110
|
+
identityId,
|
|
90
111
|
callerClass: "user",
|
|
91
112
|
issuer: "",
|
|
92
113
|
rawToken: token,
|
|
@@ -96,3 +117,25 @@ export function newApiKeyIdentityVerifier(
|
|
|
96
117
|
},
|
|
97
118
|
};
|
|
98
119
|
}
|
|
120
|
+
|
|
121
|
+
/** Stamps the accepted key's last use on its own row, atomically. */
|
|
122
|
+
function recordKeyUse(
|
|
123
|
+
store: Store,
|
|
124
|
+
logger: Logger,
|
|
125
|
+
key: ApiKey,
|
|
126
|
+
now: Date,
|
|
127
|
+
): Promise<void> {
|
|
128
|
+
const id = key.metadata?.id ?? "";
|
|
129
|
+
return recordCredentialUse({
|
|
130
|
+
credential: "api key",
|
|
131
|
+
id,
|
|
132
|
+
lastUsedAt: key.status?.lastUsedAt,
|
|
133
|
+
now,
|
|
134
|
+
logger,
|
|
135
|
+
write: (stamp) =>
|
|
136
|
+
store.updateResource(ApiResourceKind.api_key, id, ApiKeySchema, (live) => {
|
|
137
|
+
live.status ??= create(ApiKeyStatusSchema);
|
|
138
|
+
stamp(live.status);
|
|
139
|
+
}),
|
|
140
|
+
});
|
|
141
|
+
}
|
|
@@ -15,13 +15,17 @@
|
|
|
15
15
|
* nothing; a failed create leaves the grant, and the next mint completes
|
|
16
16
|
* the account; a concurrent winner is read back;
|
|
17
17
|
* - the default role is viewer, owner is refused, and the token carries
|
|
18
|
-
* the cloud's claim set for the account
|
|
18
|
+
* the cloud's claim set for the account;
|
|
19
|
+
* - a successful mint stamps the client's last use through the port's
|
|
20
|
+
* atomic modifyById (never the whole-row update), once per resolution;
|
|
21
|
+
* a refused mint records nothing; a stamp that cannot be written is
|
|
22
|
+
* logged and the token is still answered (stigmer/stigmer#1255).
|
|
19
23
|
*/
|
|
20
24
|
import { generateKeyPairSync } from "node:crypto";
|
|
21
25
|
|
|
22
26
|
import { Code, ConnectError } from "@connectrpc/connect";
|
|
23
|
-
import { create } from "@bufbuild/protobuf";
|
|
24
|
-
import { timestampFromDate } from "@bufbuild/protobuf/wkt";
|
|
27
|
+
import { clone, create } from "@bufbuild/protobuf";
|
|
28
|
+
import { timestampDate, timestampFromDate } from "@bufbuild/protobuf/wkt";
|
|
25
29
|
import { describe, expect, it } from "vitest";
|
|
26
30
|
|
|
27
31
|
import type { IdentityAccount } from "@stigmer/protos/ai/stigmer/iam/identityaccount/v1/api_pb";
|
|
@@ -30,12 +34,18 @@ import { IdentityAccountProvisioningMode } from "@stigmer/protos/ai/stigmer/iam/
|
|
|
30
34
|
import { IamPolicySchema } from "@stigmer/protos/ai/stigmer/iam/iampolicy/v1/api_pb";
|
|
31
35
|
import type { IamPolicySpec } from "@stigmer/protos/ai/stigmer/iam/iampolicy/v1/spec_pb";
|
|
32
36
|
import type { PlatformClient } from "@stigmer/protos/ai/stigmer/iam/platformclient/v1/api_pb";
|
|
33
|
-
import {
|
|
37
|
+
import {
|
|
38
|
+
PlatformClientSchema,
|
|
39
|
+
PlatformClientStatusSchema,
|
|
40
|
+
} from "@stigmer/protos/ai/stigmer/iam/platformclient/v1/api_pb";
|
|
34
41
|
import { MintUserTokenRequestSchema } from "@stigmer/protos/ai/stigmer/iam/platformclient/v1/token_pb";
|
|
35
42
|
import { IamRole } from "@stigmer/protos/ai/stigmer/iam/v1/enum_pb";
|
|
36
43
|
|
|
44
|
+
import { createLogger } from "../../../boot/logger.js";
|
|
45
|
+
import type { Logger } from "../../../boot/logger.js";
|
|
37
46
|
import type { CallerIdentity } from "../../../extensions/identity.js";
|
|
38
47
|
import { silentLogger } from "../../../extensions/__tests__/composed-support.js";
|
|
48
|
+
import { LAST_USED_RESOLUTION_MS } from "../../../identity/credential-use.js";
|
|
39
49
|
import { verifyPlatformToken } from "../../../platformtoken/envelope.js";
|
|
40
50
|
import { platformTokenKeyRingFromPem } from "../../../platformtoken/key-ring.js";
|
|
41
51
|
import type { PlatformTokenKeyRing } from "../../../platformtoken/key-ring.js";
|
|
@@ -101,6 +111,10 @@ interface Harness {
|
|
|
101
111
|
}>;
|
|
102
112
|
readonly granted: Array<{ spec: IamPolicySpec; caller: CallerIdentity }>;
|
|
103
113
|
readonly accounts: Map<string, IdentityAccount>;
|
|
114
|
+
/** The client row as the port holds it; modifyById writes here. */
|
|
115
|
+
readonly stored: { client: PlatformClient };
|
|
116
|
+
/** Every port write, by method: the stamp must ride modifyById alone. */
|
|
117
|
+
readonly clientWrites: string[];
|
|
104
118
|
}
|
|
105
119
|
|
|
106
120
|
function harness(
|
|
@@ -109,9 +123,12 @@ function harness(
|
|
|
109
123
|
keys?: PlatformTokenKeyRing | undefined;
|
|
110
124
|
failGrant?: boolean;
|
|
111
125
|
failCreate?: "fault" | "lost-race";
|
|
126
|
+
failStamp?: boolean;
|
|
127
|
+
logger?: Logger;
|
|
112
128
|
} = {},
|
|
113
129
|
): Harness {
|
|
114
|
-
const
|
|
130
|
+
const stored = { client: options.client ?? platformClient() };
|
|
131
|
+
const clientWrites: string[] = [];
|
|
115
132
|
const events: string[] = [];
|
|
116
133
|
const created: Harness["created"] = [];
|
|
117
134
|
const granted: Harness["granted"] = [];
|
|
@@ -126,12 +143,25 @@ function harness(
|
|
|
126
143
|
};
|
|
127
144
|
const deps: PlatformClientMintDeps = {
|
|
128
145
|
clients: {
|
|
129
|
-
save: async () => {
|
|
130
|
-
|
|
146
|
+
save: async () => {
|
|
147
|
+
clientWrites.push("save");
|
|
148
|
+
},
|
|
149
|
+
update: async () => {
|
|
150
|
+
clientWrites.push("update");
|
|
151
|
+
},
|
|
152
|
+
modifyById: async (id, modify) => {
|
|
153
|
+
clientWrites.push("modifyById");
|
|
154
|
+
if (options.failStamp === true) throw new Error("client store down");
|
|
155
|
+
if (id !== stored.client.metadata?.id) return undefined;
|
|
156
|
+
const next = clone(PlatformClientSchema, stored.client);
|
|
157
|
+
modify(next);
|
|
158
|
+
stored.client = next;
|
|
159
|
+
return next;
|
|
160
|
+
},
|
|
131
161
|
deleteById: async () => {},
|
|
132
|
-
findById: async () => client,
|
|
162
|
+
findById: async () => stored.client,
|
|
133
163
|
findByClientId: async (clientId) =>
|
|
134
|
-
clientId === client.spec?.clientId ? client : undefined,
|
|
164
|
+
clientId === stored.client.spec?.clientId ? stored.client : undefined,
|
|
135
165
|
findByOrgAndSlug: async () => undefined,
|
|
136
166
|
findByOrg: async () => [],
|
|
137
167
|
},
|
|
@@ -156,10 +186,10 @@ function harness(
|
|
|
156
186
|
},
|
|
157
187
|
grantPath,
|
|
158
188
|
keys: "keys" in options ? options.keys : RING,
|
|
159
|
-
logger: silentLogger,
|
|
189
|
+
logger: options.logger ?? silentLogger,
|
|
160
190
|
now: () => NOW,
|
|
161
191
|
};
|
|
162
|
-
return { deps, events, created, granted, accounts };
|
|
192
|
+
return { deps, events, created, granted, accounts, stored, clientWrites };
|
|
163
193
|
}
|
|
164
194
|
|
|
165
195
|
function request(
|
|
@@ -419,3 +449,59 @@ describe("mintUserToken — the user", () => {
|
|
|
419
449
|
);
|
|
420
450
|
});
|
|
421
451
|
});
|
|
452
|
+
|
|
453
|
+
describe("mintUserToken — the client's last use", () => {
|
|
454
|
+
it("a successful mint stamps the client's last use at the mint's clock, through modifyById alone", async () => {
|
|
455
|
+
const h = harness();
|
|
456
|
+
await mintUserToken(h.deps, request());
|
|
457
|
+
expect(h.clientWrites).toEqual(["modifyById"]);
|
|
458
|
+
expect(timestampDate(h.stored.client.status!.lastUsedAt!)).toEqual(NOW);
|
|
459
|
+
expect(h.stored.client.status?.audit?.statusAudit?.event).toBe("updated");
|
|
460
|
+
expect(
|
|
461
|
+
h.stored.client.spec?.clientSecretHash,
|
|
462
|
+
"the stamp never rewrites the credential",
|
|
463
|
+
).toBe(hashClientSecret(SECRET));
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
it("a mint inside the resolution writes nothing", async () => {
|
|
467
|
+
const recent = platformClient();
|
|
468
|
+
recent.status = create(PlatformClientStatusSchema, {
|
|
469
|
+
lastUsedAt: timestampFromDate(
|
|
470
|
+
new Date(NOW.getTime() - LAST_USED_RESOLUTION_MS / 2),
|
|
471
|
+
),
|
|
472
|
+
});
|
|
473
|
+
const h = harness({ client: recent });
|
|
474
|
+
await mintUserToken(h.deps, request());
|
|
475
|
+
expect(h.clientWrites).toEqual([]);
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
it("a refused mint records nothing", async () => {
|
|
479
|
+
const h = harness();
|
|
480
|
+
await refusal(mintUserToken(h.deps, request({ clientSecret: "wrong" })));
|
|
481
|
+
await refusal(mintUserToken(h.deps, request({ orgId: "globex" })));
|
|
482
|
+
expect(h.clientWrites).toEqual([]);
|
|
483
|
+
expect(h.stored.client.status?.lastUsedAt).toBeUndefined();
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
it("a stamp that cannot be written is logged with the client's id and the token is still answered", async () => {
|
|
487
|
+
const lines: string[] = [];
|
|
488
|
+
const h = harness({
|
|
489
|
+
failStamp: true,
|
|
490
|
+
logger: createLogger({
|
|
491
|
+
level: "warn",
|
|
492
|
+
pretty: false,
|
|
493
|
+
write: (line) => lines.push(line),
|
|
494
|
+
}),
|
|
495
|
+
});
|
|
496
|
+
const response = await mintUserToken(h.deps, request());
|
|
497
|
+
expect(response.accessToken).not.toBe("");
|
|
498
|
+
expect(lines).toHaveLength(1);
|
|
499
|
+
expect(JSON.parse(lines[0]!)).toMatchObject({
|
|
500
|
+
level: "warn",
|
|
501
|
+
credential: "platform client",
|
|
502
|
+
id: "pcl_dashboard",
|
|
503
|
+
error: "client store down",
|
|
504
|
+
});
|
|
505
|
+
expect(lines[0], "the secret is never logged").not.toContain(SECRET);
|
|
506
|
+
});
|
|
507
|
+
});
|
|
@@ -39,6 +39,10 @@ const CONTRACT_CASE_NAMES = [
|
|
|
39
39
|
"the same slug in another organization is a different client",
|
|
40
40
|
"update replaces the row in place",
|
|
41
41
|
"update of an unknown id writes nothing",
|
|
42
|
+
"modifyById persists what modify did, answers the persisted row, and the lookups follow it",
|
|
43
|
+
"modifyById of an unknown id answers undefined and writes nothing",
|
|
44
|
+
"a modify that throws writes nothing and rejects with its error",
|
|
45
|
+
"concurrent modifyById calls on one row lose no write",
|
|
42
46
|
"deleteById frees the id, the client_id and the slug; deleting an unknown id resolves",
|
|
43
47
|
"findByOrg answers every client of the organization and no other",
|
|
44
48
|
"a disconnected store is an infrastructure fault, never 'not found'",
|