@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.
Files changed (62) hide show
  1. package/dist/boot/compose.d.ts.map +1 -1
  2. package/dist/boot/compose.js +8 -1
  3. package/dist/boot/compose.js.map +1 -1
  4. package/dist/domain/apikey/controller.d.ts.map +1 -1
  5. package/dist/domain/apikey/controller.js +7 -2
  6. package/dist/domain/apikey/controller.js.map +1 -1
  7. package/dist/domain/apikey/verifier.d.ts +5 -0
  8. package/dist/domain/apikey/verifier.d.ts.map +1 -1
  9. package/dist/domain/apikey/verifier.js +32 -3
  10. package/dist/domain/apikey/verifier.js.map +1 -1
  11. package/dist/domain/platformclient/mint.d.ts.map +1 -1
  12. package/dist/domain/platformclient/mint.js +28 -1
  13. package/dist/domain/platformclient/mint.js.map +1 -1
  14. package/dist/domain/platformclient/resource-store.d.ts.map +1 -1
  15. package/dist/domain/platformclient/resource-store.js +13 -0
  16. package/dist/domain/platformclient/resource-store.js.map +1 -1
  17. package/dist/domain/platformclient/store-contract.d.ts.map +1 -1
  18. package/dist/domain/platformclient/store-contract.js +59 -0
  19. package/dist/domain/platformclient/store-contract.js.map +1 -1
  20. package/dist/domain/platformclient/store.d.ts +20 -0
  21. package/dist/domain/platformclient/store.d.ts.map +1 -1
  22. package/dist/domain/platformclient/store.js.map +1 -1
  23. package/dist/identity/credential-use.d.ts +74 -0
  24. package/dist/identity/credential-use.d.ts.map +1 -0
  25. package/dist/identity/credential-use.js +59 -0
  26. package/dist/identity/credential-use.js.map +1 -0
  27. package/dist/pipeline/steps/defaults.d.ts +10 -0
  28. package/dist/pipeline/steps/defaults.d.ts.map +1 -1
  29. package/dist/pipeline/steps/defaults.js +25 -0
  30. package/dist/pipeline/steps/defaults.js.map +1 -1
  31. package/dist/temporal/schedule/activities.d.ts.map +1 -1
  32. package/dist/temporal/schedule/activities.js +2 -1
  33. package/dist/temporal/schedule/activities.js.map +1 -1
  34. package/dist/temporal/schedule/run-starter.d.ts.map +1 -1
  35. package/dist/temporal/schedule/run-starter.js +2 -1
  36. package/dist/temporal/schedule/run-starter.js.map +1 -1
  37. package/dist/temporal/schedule/status-writes.d.ts +0 -6
  38. package/dist/temporal/schedule/status-writes.d.ts.map +1 -1
  39. package/dist/temporal/schedule/status-writes.js +8 -27
  40. package/dist/temporal/schedule/status-writes.js.map +1 -1
  41. package/dist/temporal/schedule/syncer.d.ts.map +1 -1
  42. package/dist/temporal/schedule/syncer.js +2 -1
  43. package/dist/temporal/schedule/syncer.js.map +1 -1
  44. package/package.json +6 -6
  45. package/src/boot/compose.ts +8 -1
  46. package/src/domain/apikey/__tests__/apikey.test.ts +17 -2
  47. package/src/domain/apikey/__tests__/verifier.test.ts +86 -3
  48. package/src/domain/apikey/controller.ts +12 -2
  49. package/src/domain/apikey/verifier.ts +46 -3
  50. package/src/domain/platformclient/__tests__/mint.test.ts +97 -11
  51. package/src/domain/platformclient/__tests__/resource-store.test.ts +4 -0
  52. package/src/domain/platformclient/mint.ts +34 -1
  53. package/src/domain/platformclient/resource-store.ts +18 -0
  54. package/src/domain/platformclient/store-contract.ts +100 -0
  55. package/src/domain/platformclient/store.ts +23 -0
  56. package/src/identity/__tests__/credential-use.test.ts +186 -0
  57. package/src/identity/credential-use.ts +130 -0
  58. package/src/pipeline/steps/defaults.ts +27 -0
  59. package/src/temporal/schedule/activities.ts +2 -1
  60. package/src/temporal/schedule/run-starter.ts +2 -1
  61. package/src/temporal/schedule/status-writes.ts +8 -31
  62. package/src/temporal/schedule/syncer.ts +2 -1
@@ -1,20 +1,16 @@
1
1
  /**
2
- * The clock's status-write helpers — port the ensureStatus/bumpStatusAudit
3
- * pair of pkg/domain/schedule/temporal/syncer.go.
2
+ * The clock's status-write helper — ports ensureStatus of
3
+ * pkg/domain/schedule/temporal/syncer.go.
4
4
  *
5
- * bumpStatusAudit is DELIBERATELY narrower than the shared
6
- * setAuditFieldsForUpdate: Go's clock stamps only status-audit
7
- * updated_at + event ("the same two leaves every cloud runtime patch
8
- * bumps") and never an actor — the clock is the platform, not an operator.
9
- * Controller-side status writes (trigger's last_fire_at stamp, resume's
10
- * clear) use the FULL shared helper instead, exactly as Go splits
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;;;;;;;;;;;;GAYG;AACH,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAC5C,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAGtD,OAAO,EAAE,oBAAoB,EAAE,MAAM,0DAA0D,CAAC;AAEhG,OAAO,EACL,0BAA0B,EAC1B,sBAAsB,GACvB,MAAM,0DAA0D,CAAC;AAElE;;;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;AAED;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,MAAsB;IACpD,IAAI,MAAM,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;QAC/B,MAAM,CAAC,KAAK,GAAG,MAAM,CAAC,sBAAsB,CAAC,CAAC;IAChD,CAAC;IACD,IAAI,MAAM,CAAC,KAAK,CAAC,WAAW,KAAK,SAAS,EAAE,CAAC;QAC3C,MAAM,CAAC,KAAK,CAAC,WAAW,GAAG,MAAM,CAAC,0BAA0B,CAAC,CAAC;IAChE,CAAC;IACD,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC,SAAS,GAAG,YAAY,EAAE,CAAC;IACpD,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC,KAAK,GAAG,SAAS,CAAC;AAC7C,CAAC"}
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;AAEnD,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"}
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 { bumpStatusAudit, ensureStatus } from "./status-writes.js";
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":"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"}
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,eAAe,EAAE,MAAM,kCAAkC,CAAC;AACnE,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,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAElD;;;;GAIG;AACH,MAAM,OAAO,wBAAyB,SAAQ,KAAK;IACjD;QACE,KAAK,CAAC,2BAA2B,CAAC,CAAC;QACnC,IAAI,CAAC,IAAI,GAAG,0BAA0B,CAAC;IACzC,CAAC;CACF;AAED,MAAM,OAAO,cAAc;IAGN;IACA;IACA;IACA;IALnB;IACE,+DAA+D;IAC9C,cAAwC,EACxC,KAAY,EACZ,QAA0B,EAC1B,MAAc;QAHd,mBAAc,GAAd,cAAc,CAA0B;QACxC,UAAK,GAAL,KAAK,CAAO;QACZ,aAAQ,GAAR,QAAQ,CAAkB;QAC1B,WAAM,GAAN,MAAM,CAAQ;IAC9B,CAAC;IAEJ;;;;;OAKG;IACH,KAAK,CAAC,eAAe,CAAC,QAAkB;QACtC,MAAM,MAAM,GAAG,IAAI,CAAC,cAAc,EAAE,CAAC;QACrC,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;YACzB,MAAM,IAAI,wBAAwB,EAAE,CAAC;QACvC,CAAC;QAED,MAAM,UAAU,GAAG,QAAQ,CAAC,QAAQ,EAAE,EAAE,IAAI,EAAE,CAAC;QAC/C,MAAM,EAAE,GAAG,UAAU,CAAC,UAAU,CAAC,CAAC;QAElC,IAAI,CAAC;YACH,MAAM,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC,CAAC;YACpE,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,2BAA2B,EAAE;gBAC5C,WAAW,EAAE,EAAE;gBACf,IAAI,EAAE,IAAI,CAAC,QAAQ,CAAC;gBACpB,MAAM,EAAE,aAAa,CAAC,QAAQ,CAAC;aAChC,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,CAAC,CAAC,KAAK,YAAY,sBAAsB,CAAC,EAAE,CAAC;gBAC/C,MAAM,IAAI,KAAK,CAAC,4BAA4B,EAAE,KAAK,OAAO,CAAC,KAAK,CAAC,EAAE,EAAE;oBACnE,KAAK,EAAE,KAAK;iBACb,CAAC,CAAC;YACL,CAAC;YACD,qEAAqE;YACrE,sEAAsE;YACtE,qCAAqC;YACrC,IAAI,CAAC;gBACH,MAAM,MAAM,CAAC,QAAQ;qBAClB,SAAS,CAAC,EAAE,CAAC;qBACb,MAAM,CAAC,CAAC,QAA6B,EAAE,EAAE,CACxC,IAAI,CAAC,QAAQ,CAAC,iBAAiB,CAAC,QAAQ,EAAE,QAAQ,CAAC,CACpD,CAAC;YACN,CAAC;YAAC,OAAO,WAAW,EAAE,CAAC;gBACrB,MAAM,IAAI,KAAK,CACb,4BAA4B,EAAE,KAAK,OAAO,CAAC,WAAW,CAAC,EAAE,EACzD,EAAE,KAAK,EAAE,WAAW,EAAE,CACvB,CAAC;YACJ,CAAC;YACD,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,2BAA2B,EAAE;gBAC5C,WAAW,EAAE,EAAE;gBACf,IAAI,EAAE,IAAI,CAAC,QAAQ,CAAC;gBACpB,MAAM,EAAE,aAAa,CAAC,QAAQ,CAAC;aAChC,CAAC,CAAC;QACL,CAAC;QAED,OAAO,IAAI,CAAC,gBAAgB,CAAC,QAAQ,CAAC,CAAC;IACzC,CAAC;IAED;;;;;OAKG;IACH,KAAK,CAAC,cAAc,CAAC,QAAkB;QACrC,IAAI,aAAa,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC5B,OAAO,SAAS,CAAC;QACnB,CAAC;QACD,MAAM,MAAM,GAAG,IAAI,CAAC,cAAc,EAAE,CAAC;QACrC,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;YACzB,MAAM,IAAI,wBAAwB,EAAE,CAAC;QACvC,CAAC;QACD,IAAI,WAAgC,CAAC;QACrC,IAAI,CAAC;YACH,WAAW,GAAG,MAAM,MAAM,CAAC,QAAQ;iBAChC,SAAS,CAAC,UAAU,CAAC,QAAQ,CAAC,QAAQ,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC;iBAClD,QAAQ,EAAE,CAAC;QAChB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,IAAI,KAAK,CAAC,+BAA+B,OAAO,CAAC,KAAK,CAAC,EAAE,EAAE;gBAC/D,KAAK,EAAE,KAAK;aACb,CAAC,CAAC;QACL,CAAC;QACD,OAAO,WAAW,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;IAC7C,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,QAAQ,CAAC,UAAkB;QAC/B,MAAM,MAAM,GAAG,IAAI,CAAC,cAAc,EAAE,CAAC;QACrC,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;YACzB,MAAM,IAAI,wBAAwB,EAAE,CAAC;QACvC,CAAC;QACD,MAAM,EAAE,GAAG,UAAU,CAAC,UAAU,CAAC,CAAC;QAClC,IAAI,CAAC;YACH,MAAM,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC;QAC/C,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,KAAK,YAAY,qBAAqB,EAAE,CAAC;gBAC3C,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,gCAAgC,EAAE,EAAE,WAAW,EAAE,EAAE,EAAE,CAAC,CAAC;gBACxE,OAAO;YACT,CAAC;YACD,MAAM,IAAI,KAAK,CAAC,4BAA4B,EAAE,KAAK,OAAO,CAAC,KAAK,CAAC,EAAE,EAAE;gBACnE,KAAK,EAAE,KAAK;aACb,CAAC,CAAC;QACL,CAAC;QACD,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,2BAA2B,EAAE,EAAE,WAAW,EAAE,EAAE,EAAE,CAAC,CAAC;IACrE,CAAC;IAED;;;;OAIG;IACK,KAAK,CAAC,gBAAgB,CAAC,QAAkB;QAC/C,MAAM,UAAU,GAAG,MAAM,IAAI,CAAC,cAAc,CAAC,QAAQ,CAAC,CAAC;QAEvD,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,KAAK,CAAC,cAAc,CAC7B,eAAe,CAAC,QAAQ,EACxB,QAAQ,CAAC,QAAQ,EAAE,EAAE,IAAI,EAAE,EAC3B,cAAc,EACd,CAAC,IAAI,EAAE,EAAE;gBACP,MAAM,MAAM,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;gBAClC,MAAM,CAAC,UAAU;oBACf,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,iBAAiB,CAAC,UAAU,CAAC,CAAC;gBACvE,eAAe,CAAC,MAAM,CAAC,CAAC;YAC1B,CAAC,CACF,CAAC;QACJ,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,KAAK,YAAY,qBAAqB,EAAE,CAAC;gBAC3C,mEAAmE;gBACnE,iEAAiE;gBACjE,IAAI,CAAC,MAAM,CAAC,IAAI,CACd,wDAAwD,EACxD,EAAE,WAAW,EAAE,QAAQ,CAAC,QAAQ,EAAE,EAAE,IAAI,EAAE,EAAE,CAC7C,CAAC;gBACF,OAAO,UAAU,CAAC;YACpB,CAAC;YACD,MAAM,IAAI,KAAK,CAAC,uBAAuB,OAAO,CAAC,KAAK,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;QAC7E,CAAC;QACD,OAAO,UAAU,CAAC;IACpB,CAAC;CACF;AAED,SAAS,OAAO,CAAC,KAAc;IAC7B,OAAO,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAChE,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stigmer/server",
3
- "version": "3.32.0",
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.32.0",
67
- "@stigmer/plugin-package": "3.32.0",
68
- "@stigmer/protos": "3.32.0",
69
- "@stigmer/temporal-codecs": "3.32.0",
70
- "@stigmer/zip-structure": "3.32.0",
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",
@@ -1693,7 +1693,14 @@ export async function composeServer(
1693
1693
  // own (the cloud's sandbox lanes).
1694
1694
  const identityVerifiers = [
1695
1695
  ...(requireAuthentication
1696
- ? [newApiKeyIdentityVerifier({ store, accounts: identityAccounts })]
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(lookup: AccountsBySubject = accounts) {
61
- return newApiKeyIdentityVerifier({ store, accounts: lookup });
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 entries = await restrictListByReadScope(
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) <= new Date()) {
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: await identityIdForSubject(accounts, owner.id),
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 { PlatformClientSchema } from "@stigmer/protos/ai/stigmer/iam/platformclient/v1/api_pb";
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 client = options.client ?? platformClient();
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
- update: async () => {},
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'",