@stratum-hq/control-plane 0.4.0 → 1.0.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 (44) hide show
  1. package/dist/middleware/authorize.d.ts.map +1 -1
  2. package/dist/middleware/authorize.js +3 -1
  3. package/dist/middleware/authorize.js.map +1 -1
  4. package/package.json +14 -1
  5. package/dist/__tests__/admin-scope-query-string.test.d.ts +0 -2
  6. package/dist/__tests__/admin-scope-query-string.test.d.ts.map +0 -1
  7. package/dist/__tests__/admin-scope-query-string.test.js +0 -69
  8. package/dist/__tests__/admin-scope-query-string.test.js.map +0 -1
  9. package/dist/__tests__/api-keys.test.d.ts +0 -2
  10. package/dist/__tests__/api-keys.test.d.ts.map +0 -1
  11. package/dist/__tests__/api-keys.test.js +0 -240
  12. package/dist/__tests__/api-keys.test.js.map +0 -1
  13. package/dist/__tests__/auth.test.d.ts +0 -2
  14. package/dist/__tests__/auth.test.d.ts.map +0 -1
  15. package/dist/__tests__/auth.test.js +0 -229
  16. package/dist/__tests__/auth.test.js.map +0 -1
  17. package/dist/__tests__/config.test.d.ts +0 -2
  18. package/dist/__tests__/config.test.d.ts.map +0 -1
  19. package/dist/__tests__/config.test.js +0 -256
  20. package/dist/__tests__/config.test.js.map +0 -1
  21. package/dist/__tests__/configdiff-roles-scope.test.d.ts +0 -2
  22. package/dist/__tests__/configdiff-roles-scope.test.d.ts.map +0 -1
  23. package/dist/__tests__/configdiff-roles-scope.test.js +0 -246
  24. package/dist/__tests__/configdiff-roles-scope.test.js.map +0 -1
  25. package/dist/__tests__/default-deny.test.d.ts +0 -2
  26. package/dist/__tests__/default-deny.test.d.ts.map +0 -1
  27. package/dist/__tests__/default-deny.test.js +0 -92
  28. package/dist/__tests__/default-deny.test.js.map +0 -1
  29. package/dist/__tests__/tenant-create-scope.test.d.ts +0 -2
  30. package/dist/__tests__/tenant-create-scope.test.d.ts.map +0 -1
  31. package/dist/__tests__/tenant-create-scope.test.js +0 -129
  32. package/dist/__tests__/tenant-create-scope.test.js.map +0 -1
  33. package/dist/__tests__/tenant-move-scope.test.d.ts +0 -2
  34. package/dist/__tests__/tenant-move-scope.test.d.ts.map +0 -1
  35. package/dist/__tests__/tenant-move-scope.test.js +0 -113
  36. package/dist/__tests__/tenant-move-scope.test.js.map +0 -1
  37. package/dist/__tests__/tenants.test.d.ts +0 -2
  38. package/dist/__tests__/tenants.test.d.ts.map +0 -1
  39. package/dist/__tests__/tenants.test.js +0 -214
  40. package/dist/__tests__/tenants.test.js.map +0 -1
  41. package/dist/__tests__/test-helpers.d.ts +0 -71
  42. package/dist/__tests__/test-helpers.d.ts.map +0 -1
  43. package/dist/__tests__/test-helpers.js +0 -189
  44. package/dist/__tests__/test-helpers.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"authorize.d.ts","sourceRoot":"","sources":["../../src/middleware/authorize.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAwCvD,wBAAgB,yBAAyB,KAErC,SAAS,cAAc,EACvB,OAAO,YAAY,KAClB,OAAO,CAAC,IAAI,CAAC,CAsBjB"}
1
+ {"version":3,"file":"authorize.d.ts","sourceRoot":"","sources":["../../src/middleware/authorize.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAwCvD,wBAAgB,yBAAyB,KAErC,SAAS,cAAc,EACvB,OAAO,YAAY,KAClB,OAAO,CAAC,IAAI,CAAC,CAwBjB"}
@@ -48,7 +48,9 @@ function createAuthorizeMiddleware() {
48
48
  }
49
49
  const requiredScope = getRequiredScope(request.method, request.url);
50
50
  const scopes = request.apiKey.scopes ?? ["read"];
51
- if (!scopes.includes(requiredScope)) {
51
+ // Hierarchical scopes: admin implies write implies read. A granted scope
52
+ // satisfies any required scope of equal-or-lower rank.
53
+ if (!(0, core_1.scopeSatisfies)(scopes, requiredScope)) {
52
54
  throw new core_1.ForbiddenError("Insufficient permissions for this operation");
53
55
  }
54
56
  };
@@ -1 +1 @@
1
- {"version":3,"file":"authorize.js","sourceRoot":"","sources":["../../src/middleware/authorize.ts"],"names":[],"mappings":";;AAwCA,8DA0BC;AAjED,2CAAqE;AAIrE,SAAS,aAAa,CAAC,MAAc;IACnC,QAAQ,MAAM,CAAC,WAAW,EAAE,EAAE,CAAC;QAC7B,KAAK,KAAK,CAAC;QACX,KAAK,MAAM,CAAC;QACZ,KAAK,SAAS;YACZ,OAAO,MAAM,CAAC;QAChB;YACE,OAAO,OAAO,CAAC;IACnB,CAAC;AACH,CAAC;AAED,0DAA0D;AAC1D,MAAM,YAAY,GAAG;IACnB,sBAAsB;IACtB,wBAAwB;IACxB,yBAAyB;IACzB,qBAAqB;IACrB,oCAAoC;IACpC,qCAAqC;IACrC,6CAA6C;IAC7C,sCAAsC;CACvC,CAAC;AAEF,SAAS,gBAAgB,CAAC,MAAc,EAAE,GAAW;IACnD,yEAAyE;IACzE,yDAAyD;IACzD,MAAM,IAAI,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAClC,KAAK,MAAM,OAAO,IAAI,YAAY,EAAE,CAAC;QACnC,IAAI,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YACvB,OAAO,OAAO,CAAC;QACjB,CAAC;IACH,CAAC;IACD,OAAO,aAAa,CAAC,MAAM,CAAC,CAAC;AAC/B,CAAC;AAED,SAAgB,yBAAyB;IACvC,OAAO,KAAK,UAAU,mBAAmB,CACvC,OAAuB,EACvB,KAAmB;QAEnB,8CAA8C;QAC9C,IACE,OAAO,CAAC,GAAG,KAAK,gBAAgB;YAChC,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,iBAAiB,CAAC;YACzC,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,WAAW,CAAC,EACnC,CAAC;YACD,OAAO;QACT,CAAC;QAED,mEAAmE;QACnE,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC;YACpB,MAAM,IAAI,wBAAiB,CAAC,yBAAyB,CAAC,CAAC;QACzD,CAAC;QAED,MAAM,aAAa,GAAG,gBAAgB,CAAC,OAAO,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC;QACpE,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,CAAC;QAEjD,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,aAAa,CAAC,EAAE,CAAC;YACpC,MAAM,IAAI,qBAAc,CAAC,6CAA6C,CAAC,CAAC;QAC1E,CAAC;IACH,CAAC,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"authorize.js","sourceRoot":"","sources":["../../src/middleware/authorize.ts"],"names":[],"mappings":";;AAwCA,8DA4BC;AAnED,2CAAqF;AAIrF,SAAS,aAAa,CAAC,MAAc;IACnC,QAAQ,MAAM,CAAC,WAAW,EAAE,EAAE,CAAC;QAC7B,KAAK,KAAK,CAAC;QACX,KAAK,MAAM,CAAC;QACZ,KAAK,SAAS;YACZ,OAAO,MAAM,CAAC;QAChB;YACE,OAAO,OAAO,CAAC;IACnB,CAAC;AACH,CAAC;AAED,0DAA0D;AAC1D,MAAM,YAAY,GAAG;IACnB,sBAAsB;IACtB,wBAAwB;IACxB,yBAAyB;IACzB,qBAAqB;IACrB,oCAAoC;IACpC,qCAAqC;IACrC,6CAA6C;IAC7C,sCAAsC;CACvC,CAAC;AAEF,SAAS,gBAAgB,CAAC,MAAc,EAAE,GAAW;IACnD,yEAAyE;IACzE,yDAAyD;IACzD,MAAM,IAAI,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAClC,KAAK,MAAM,OAAO,IAAI,YAAY,EAAE,CAAC;QACnC,IAAI,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YACvB,OAAO,OAAO,CAAC;QACjB,CAAC;IACH,CAAC;IACD,OAAO,aAAa,CAAC,MAAM,CAAC,CAAC;AAC/B,CAAC;AAED,SAAgB,yBAAyB;IACvC,OAAO,KAAK,UAAU,mBAAmB,CACvC,OAAuB,EACvB,KAAmB;QAEnB,8CAA8C;QAC9C,IACE,OAAO,CAAC,GAAG,KAAK,gBAAgB;YAChC,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,iBAAiB,CAAC;YACzC,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,WAAW,CAAC,EACnC,CAAC;YACD,OAAO;QACT,CAAC;QAED,mEAAmE;QACnE,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC;YACpB,MAAM,IAAI,wBAAiB,CAAC,yBAAyB,CAAC,CAAC;QACzD,CAAC;QAED,MAAM,aAAa,GAAG,gBAAgB,CAAC,OAAO,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC;QACpE,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,CAAC;QAEjD,yEAAyE;QACzE,uDAAuD;QACvD,IAAI,CAAC,IAAA,qBAAc,EAAC,MAAM,EAAE,aAAa,CAAC,EAAE,CAAC;YAC3C,MAAM,IAAI,qBAAc,CAAC,6CAA6C,CAAC,CAAC;QAC1E,CAAC;IACH,CAAC,CAAC;AACJ,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stratum-hq/control-plane",
3
- "version": "0.4.0",
3
+ "version": "1.0.0",
4
4
  "description": "Stratum control plane REST API server",
5
5
  "keywords": [
6
6
  "multi-tenancy",
@@ -17,6 +17,12 @@
17
17
  ],
18
18
  "main": "./dist/index.js",
19
19
  "types": "./dist/index.d.ts",
20
+ "exports": {
21
+ ".": {
22
+ "types": "./dist/index.d.ts",
23
+ "default": "./dist/index.js"
24
+ }
25
+ },
20
26
  "files": [
21
27
  "dist",
22
28
  "README.md",
@@ -59,6 +65,13 @@
59
65
  "typescript": "^5.4.0",
60
66
  "vitest": "^1.6.0"
61
67
  },
68
+ "license": "MIT",
69
+ "author": "Christian Crank",
70
+ "homepage": "https://github.com/stratum-hq/Stratum/tree/main/packages/control-plane#readme",
71
+ "bugs": "https://github.com/stratum-hq/Stratum/issues",
72
+ "engines": {
73
+ "node": ">=20.0.0"
74
+ },
62
75
  "repository": {
63
76
  "type": "git",
64
77
  "url": "https://github.com/stratum-hq/Stratum.git",
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=admin-scope-query-string.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"admin-scope-query-string.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/admin-scope-query-string.test.ts"],"names":[],"mappings":""}
@@ -1,69 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- const vitest_1 = require("vitest");
4
- const test_helpers_js_1 = require("./test-helpers.js");
5
- /**
6
- * Admin-scope enforcement must depend on the resolved request PATH only, not on
7
- * the raw URL. A query string appended to an admin-only route must not change
8
- * which scope the route requires.
9
- */
10
- (0, vitest_1.describe)("admin scope is enforced independent of the query string", () => {
11
- let app;
12
- let stratum;
13
- const id = test_helpers_js_1.SAMPLE_TENANT.id;
14
- (0, vitest_1.beforeEach)(async () => {
15
- stratum = (0, test_helpers_js_1.createMockStratum)();
16
- stratum.getTenant.mockResolvedValue(test_helpers_js_1.SAMPLE_TENANT);
17
- stratum.purgeTenant.mockResolvedValue(undefined);
18
- // getTenantContext is not stubbed by the shared helper; add it locally.
19
- stratum.getTenantContext = vitest_1.vi
20
- .fn()
21
- .mockResolvedValue({ tenant: test_helpers_js_1.SAMPLE_TENANT });
22
- app = await (0, test_helpers_js_1.buildTestApp)(stratum);
23
- });
24
- (0, vitest_1.afterEach)(async () => {
25
- await app.close();
26
- });
27
- (0, vitest_1.it)("write-scope key is refused on an admin route (baseline, no query string)", async () => {
28
- (0, test_helpers_js_1.setupWriteApiKey)(stratum);
29
- const res = await app.inject({
30
- method: "POST",
31
- url: `/api/v1/tenants/${id}/purge`,
32
- headers: (0, test_helpers_js_1.authHeaders)(),
33
- });
34
- (0, vitest_1.expect)(res.statusCode).toBe(403);
35
- (0, vitest_1.expect)(stratum.purgeTenant).not.toHaveBeenCalled();
36
- });
37
- (0, vitest_1.it)("write-scope key stays refused on an admin route carrying a query string", async () => {
38
- (0, test_helpers_js_1.setupWriteApiKey)(stratum);
39
- const res = await app.inject({
40
- method: "POST",
41
- url: `/api/v1/tenants/${id}/purge?probe=1`,
42
- headers: (0, test_helpers_js_1.authHeaders)(),
43
- });
44
- (0, vitest_1.expect)(res.statusCode).toBe(403);
45
- (0, vitest_1.expect)(stratum.purgeTenant).not.toHaveBeenCalled();
46
- });
47
- (0, vitest_1.it)("read-scope key stays refused on an admin GET route carrying a query string", async () => {
48
- (0, test_helpers_js_1.setupReadOnlyApiKey)(stratum);
49
- const res = await app.inject({
50
- method: "GET",
51
- url: `/api/v1/tenants/${id}/context?probe=1`,
52
- headers: (0, test_helpers_js_1.authHeaders)(),
53
- });
54
- (0, vitest_1.expect)(res.statusCode).toBe(403);
55
- (0, vitest_1.expect)(stratum
56
- .getTenantContext).not.toHaveBeenCalled();
57
- });
58
- (0, vitest_1.it)("admin-scope key is still allowed on an admin route with a query string", async () => {
59
- (0, test_helpers_js_1.setupAdminApiKey)(stratum);
60
- const res = await app.inject({
61
- method: "POST",
62
- url: `/api/v1/tenants/${id}/purge?probe=1`,
63
- headers: (0, test_helpers_js_1.authHeaders)(),
64
- });
65
- (0, vitest_1.expect)(res.statusCode).toBe(204);
66
- (0, vitest_1.expect)(stratum.purgeTenant).toHaveBeenCalled();
67
- });
68
- });
69
- //# sourceMappingURL=admin-scope-query-string.test.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"admin-scope-query-string.test.js","sourceRoot":"","sources":["../../src/__tests__/admin-scope-query-string.test.ts"],"names":[],"mappings":";;AAAA,mCAAyE;AAGzE,uDAQ2B;AAE3B;;;;GAIG;AACH,IAAA,iBAAQ,EAAC,yDAAyD,EAAE,GAAG,EAAE;IACvE,IAAI,GAAoB,CAAC;IACzB,IAAI,OAAgB,CAAC;IACrB,MAAM,EAAE,GAAG,+BAAa,CAAC,EAAE,CAAC;IAE5B,IAAA,mBAAU,EAAC,KAAK,IAAI,EAAE;QACpB,OAAO,GAAG,IAAA,mCAAiB,GAAE,CAAC;QAC7B,OAAO,CAAC,SAAsC,CAAC,iBAAiB,CAC/D,+BAAa,CACd,CAAC;QACD,OAAO,CAAC,WAAwC,CAAC,iBAAiB,CACjE,SAAS,CACV,CAAC;QACF,wEAAwE;QACvE,OAA8C,CAAC,gBAAgB,GAAG,WAAE;aAClE,EAAE,EAAE;aACJ,iBAAiB,CAAC,EAAE,MAAM,EAAE,+BAAa,EAAE,CAAC,CAAC;QAChD,GAAG,GAAG,MAAM,IAAA,8BAAY,EAAC,OAAO,CAAC,CAAC;IACpC,CAAC,CAAC,CAAC;IAEH,IAAA,kBAAS,EAAC,KAAK,IAAI,EAAE;QACnB,MAAM,GAAG,CAAC,KAAK,EAAE,CAAC;IACpB,CAAC,CAAC,CAAC;IAEH,IAAA,WAAE,EAAC,0EAA0E,EAAE,KAAK,IAAI,EAAE;QACxF,IAAA,kCAAgB,EAAC,OAAO,CAAC,CAAC;QAE1B,MAAM,GAAG,GAAG,MAAM,GAAG,CAAC,MAAM,CAAC;YAC3B,MAAM,EAAE,MAAM;YACd,GAAG,EAAE,mBAAmB,EAAE,QAAQ;YAClC,OAAO,EAAE,IAAA,6BAAW,GAAE;SACvB,CAAC,CAAC;QAEH,IAAA,eAAM,EAAC,GAAG,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACjC,IAAA,eAAM,EAAC,OAAO,CAAC,WAAW,CAAC,CAAC,GAAG,CAAC,gBAAgB,EAAE,CAAC;IACrD,CAAC,CAAC,CAAC;IAEH,IAAA,WAAE,EAAC,yEAAyE,EAAE,KAAK,IAAI,EAAE;QACvF,IAAA,kCAAgB,EAAC,OAAO,CAAC,CAAC;QAE1B,MAAM,GAAG,GAAG,MAAM,GAAG,CAAC,MAAM,CAAC;YAC3B,MAAM,EAAE,MAAM;YACd,GAAG,EAAE,mBAAmB,EAAE,gBAAgB;YAC1C,OAAO,EAAE,IAAA,6BAAW,GAAE;SACvB,CAAC,CAAC;QAEH,IAAA,eAAM,EAAC,GAAG,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACjC,IAAA,eAAM,EAAC,OAAO,CAAC,WAAW,CAAC,CAAC,GAAG,CAAC,gBAAgB,EAAE,CAAC;IACrD,CAAC,CAAC,CAAC;IAEH,IAAA,WAAE,EAAC,4EAA4E,EAAE,KAAK,IAAI,EAAE;QAC1F,IAAA,qCAAmB,EAAC,OAAO,CAAC,CAAC;QAE7B,MAAM,GAAG,GAAG,MAAM,GAAG,CAAC,MAAM,CAAC;YAC3B,MAAM,EAAE,KAAK;YACb,GAAG,EAAE,mBAAmB,EAAE,kBAAkB;YAC5C,OAAO,EAAE,IAAA,6BAAW,GAAE;SACvB,CAAC,CAAC;QAEH,IAAA,eAAM,EAAC,GAAG,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACjC,IAAA,eAAM,EACH,OAAqE;aACnE,gBAAgB,CACpB,CAAC,GAAG,CAAC,gBAAgB,EAAE,CAAC;IAC3B,CAAC,CAAC,CAAC;IAEH,IAAA,WAAE,EAAC,wEAAwE,EAAE,KAAK,IAAI,EAAE;QACtF,IAAA,kCAAgB,EAAC,OAAO,CAAC,CAAC;QAE1B,MAAM,GAAG,GAAG,MAAM,GAAG,CAAC,MAAM,CAAC;YAC3B,MAAM,EAAE,MAAM;YACd,GAAG,EAAE,mBAAmB,EAAE,gBAAgB;YAC1C,OAAO,EAAE,IAAA,6BAAW,GAAE;SACvB,CAAC,CAAC;QAEH,IAAA,eAAM,EAAC,GAAG,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACjC,IAAA,eAAM,EAAC,OAAO,CAAC,WAAW,CAAC,CAAC,gBAAgB,EAAE,CAAC;IACjD,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=api-keys.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"api-keys.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/api-keys.test.ts"],"names":[],"mappings":""}
@@ -1,240 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- const vitest_1 = require("vitest");
4
- const test_helpers_js_1 = require("./test-helpers.js");
5
- (0, vitest_1.describe)("API Key Routes", () => {
6
- let app;
7
- let stratum;
8
- (0, vitest_1.beforeEach)(async () => {
9
- stratum = (0, test_helpers_js_1.createMockStratum)();
10
- (0, test_helpers_js_1.setupAdminApiKey)(stratum);
11
- app = await (0, test_helpers_js_1.buildTestApp)(stratum);
12
- });
13
- (0, vitest_1.afterEach)(async () => {
14
- await app.close();
15
- });
16
- // ── POST /api/v1/api-keys ──────────────────────────────────────────
17
- (0, vitest_1.describe)("POST /api/v1/api-keys", () => {
18
- (0, vitest_1.it)("creates a key and returns plaintext once with 201", async () => {
19
- const createdKey = {
20
- id: "key-uuid-1",
21
- tenant_id: "550e8400-e29b-41d4-a716-446655440000",
22
- plaintext: "sk_test_abc123def456",
23
- name: "My API Key",
24
- created_at: new Date().toISOString(),
25
- };
26
- stratum.createApiKey.mockResolvedValue(createdKey);
27
- const response = await app.inject({
28
- method: "POST",
29
- url: "/api/v1/api-keys",
30
- headers: (0, test_helpers_js_1.authHeaders)(),
31
- payload: { tenant_id: "550e8400-e29b-41d4-a716-446655440000", name: "My API Key" },
32
- });
33
- (0, vitest_1.expect)(response.statusCode).toBe(201);
34
- const body = response.json();
35
- (0, vitest_1.expect)(body.id).toBe("key-uuid-1");
36
- (0, vitest_1.expect)(body.plaintext).toBe("sk_test_abc123def456");
37
- (0, vitest_1.expect)(stratum.createApiKey).toHaveBeenCalledOnce();
38
- (0, vitest_1.expect)(stratum.createApiKey.mock.calls[0][0]).toBe("550e8400-e29b-41d4-a716-446655440000");
39
- });
40
- (0, vitest_1.it)("returns 400 for missing tenant_id", async () => {
41
- const response = await app.inject({
42
- method: "POST",
43
- url: "/api/v1/api-keys",
44
- headers: (0, test_helpers_js_1.authHeaders)(),
45
- payload: { name: "No Tenant" },
46
- });
47
- (0, vitest_1.expect)(response.statusCode).toBe(400);
48
- const body = response.json();
49
- (0, vitest_1.expect)(body.error.code).toBe("VALIDATION_ERROR");
50
- });
51
- (0, vitest_1.it)("returns 400 for invalid tenant_id (not UUID)", async () => {
52
- const response = await app.inject({
53
- method: "POST",
54
- url: "/api/v1/api-keys",
55
- headers: (0, test_helpers_js_1.authHeaders)(),
56
- payload: { tenant_id: "not-a-uuid", name: "Bad ID" },
57
- });
58
- (0, vitest_1.expect)(response.statusCode).toBe(400);
59
- });
60
- (0, vitest_1.it)("creates a key with rate limit options", async () => {
61
- const createdKey = {
62
- id: "key-uuid-2",
63
- tenant_id: "550e8400-e29b-41d4-a716-446655440000",
64
- plaintext: "sk_test_rate_limited",
65
- name: "Rate Limited Key",
66
- created_at: new Date().toISOString(),
67
- };
68
- stratum.createApiKey.mockResolvedValue(createdKey);
69
- const response = await app.inject({
70
- method: "POST",
71
- url: "/api/v1/api-keys",
72
- headers: (0, test_helpers_js_1.authHeaders)(),
73
- payload: {
74
- tenant_id: "550e8400-e29b-41d4-a716-446655440000",
75
- name: "Rate Limited Key",
76
- rate_limit_max: 500,
77
- rate_limit_window: "1 minute",
78
- },
79
- });
80
- (0, vitest_1.expect)(response.statusCode).toBe(201);
81
- (0, vitest_1.expect)(stratum.createApiKey).toHaveBeenCalledWith("550e8400-e29b-41d4-a716-446655440000", {
82
- name: "Rate Limited Key",
83
- rateLimitMax: 500,
84
- rateLimitWindow: "1 minute",
85
- });
86
- });
87
- });
88
- // ── GET /api/v1/api-keys ───────────────────────────────────────────
89
- (0, vitest_1.describe)("GET /api/v1/api-keys", () => {
90
- (0, vitest_1.it)("lists keys without plaintext", async () => {
91
- const keys = [
92
- {
93
- id: "key-uuid-1",
94
- tenant_id: "550e8400-e29b-41d4-a716-446655440000",
95
- name: "Key 1",
96
- created_at: new Date().toISOString(),
97
- last_used_at: null,
98
- revoked_at: null,
99
- expires_at: null,
100
- },
101
- {
102
- id: "key-uuid-2",
103
- tenant_id: "550e8400-e29b-41d4-a716-446655440000",
104
- name: "Key 2",
105
- created_at: new Date().toISOString(),
106
- last_used_at: new Date().toISOString(),
107
- revoked_at: null,
108
- expires_at: null,
109
- },
110
- ];
111
- stratum.listApiKeys.mockResolvedValue(keys);
112
- const response = await app.inject({
113
- method: "GET",
114
- url: "/api/v1/api-keys",
115
- headers: (0, test_helpers_js_1.authHeaders)(),
116
- });
117
- (0, vitest_1.expect)(response.statusCode).toBe(200);
118
- const body = response.json();
119
- (0, vitest_1.expect)(body).toHaveLength(2);
120
- // Plaintext should never appear in list response
121
- (0, vitest_1.expect)(body[0]).not.toHaveProperty("plaintext");
122
- (0, vitest_1.expect)(body[1]).not.toHaveProperty("plaintext");
123
- (0, vitest_1.expect)(body[0].id).toBe("key-uuid-1");
124
- });
125
- (0, vitest_1.it)("lists keys filtered by tenant_id query param", async () => {
126
- stratum.listApiKeys.mockResolvedValue([]);
127
- const response = await app.inject({
128
- method: "GET",
129
- url: "/api/v1/api-keys?tenant_id=550e8400-e29b-41d4-a716-446655440000",
130
- headers: (0, test_helpers_js_1.authHeaders)(),
131
- });
132
- (0, vitest_1.expect)(response.statusCode).toBe(200);
133
- // The tenant_id is passed through to listApiKeys
134
- (0, vitest_1.expect)(stratum.listApiKeys).toHaveBeenCalled();
135
- });
136
- });
137
- // ── DELETE /api/v1/api-keys/:id ────────────────────────────────────
138
- (0, vitest_1.describe)("DELETE /api/v1/api-keys/:id", () => {
139
- (0, vitest_1.it)("revokes a key and returns 204", async () => {
140
- stratum.revokeApiKey.mockResolvedValue(true);
141
- const response = await app.inject({
142
- method: "DELETE",
143
- url: "/api/v1/api-keys/key-uuid-1",
144
- headers: (0, test_helpers_js_1.authHeaders)(),
145
- });
146
- (0, vitest_1.expect)(response.statusCode).toBe(204);
147
- (0, vitest_1.expect)(response.body).toBe("");
148
- (0, vitest_1.expect)(stratum.revokeApiKey).toHaveBeenCalledWith("key-uuid-1");
149
- });
150
- (0, vitest_1.it)("returns 404 when key does not exist", async () => {
151
- stratum.revokeApiKey.mockResolvedValue(false);
152
- const response = await app.inject({
153
- method: "DELETE",
154
- url: "/api/v1/api-keys/nonexistent-key",
155
- headers: (0, test_helpers_js_1.authHeaders)(),
156
- });
157
- (0, vitest_1.expect)(response.statusCode).toBe(404);
158
- const body = response.json();
159
- (0, vitest_1.expect)(body.error.code).toBe("NOT_FOUND");
160
- });
161
- });
162
- // ── POST /api/v1/api-keys/:id/rotate ──────────────────────────────
163
- (0, vitest_1.describe)("POST /api/v1/api-keys/:id/rotate", () => {
164
- (0, vitest_1.it)("rotates a key and returns new plaintext with 201", async () => {
165
- const rotatedKey = {
166
- id: "key-uuid-new",
167
- tenant_id: "550e8400-e29b-41d4-a716-446655440000",
168
- plaintext: "sk_test_rotated_xyz789",
169
- name: "Rotated Key",
170
- created_at: new Date().toISOString(),
171
- };
172
- stratum.rotateApiKey.mockResolvedValue(rotatedKey);
173
- const response = await app.inject({
174
- method: "POST",
175
- url: "/api/v1/api-keys/key-uuid-1/rotate",
176
- headers: (0, test_helpers_js_1.authHeaders)(),
177
- payload: {},
178
- });
179
- (0, vitest_1.expect)(response.statusCode).toBe(201);
180
- const body = response.json();
181
- (0, vitest_1.expect)(body.plaintext).toBe("sk_test_rotated_xyz789");
182
- (0, vitest_1.expect)(stratum.rotateApiKey).toHaveBeenCalledWith("key-uuid-1", undefined);
183
- });
184
- (0, vitest_1.it)("rotates a key with a new name", async () => {
185
- const rotatedKey = {
186
- id: "key-uuid-new",
187
- tenant_id: "550e8400-e29b-41d4-a716-446655440000",
188
- plaintext: "sk_test_rotated_abc",
189
- name: "New Name",
190
- created_at: new Date().toISOString(),
191
- };
192
- stratum.rotateApiKey.mockResolvedValue(rotatedKey);
193
- const response = await app.inject({
194
- method: "POST",
195
- url: "/api/v1/api-keys/key-uuid-1/rotate",
196
- headers: (0, test_helpers_js_1.authHeaders)(),
197
- payload: { name: "New Name" },
198
- });
199
- (0, vitest_1.expect)(response.statusCode).toBe(201);
200
- (0, vitest_1.expect)(response.json().name).toBe("New Name");
201
- (0, vitest_1.expect)(stratum.rotateApiKey).toHaveBeenCalledWith("key-uuid-1", "New Name");
202
- });
203
- });
204
- // ── Authorization ───────────────────────────────────────────────────
205
- (0, vitest_1.describe)("Authorization", () => {
206
- (0, vitest_1.it)("api-keys routes require admin scope", async () => {
207
- (0, test_helpers_js_1.setupWriteApiKey)(stratum);
208
- const getResponse = await app.inject({
209
- method: "GET",
210
- url: "/api/v1/api-keys",
211
- headers: (0, test_helpers_js_1.authHeaders)(),
212
- });
213
- (0, vitest_1.expect)(getResponse.statusCode).toBe(403);
214
- const postResponse = await app.inject({
215
- method: "POST",
216
- url: "/api/v1/api-keys",
217
- headers: (0, test_helpers_js_1.authHeaders)(),
218
- payload: { tenant_id: "550e8400-e29b-41d4-a716-446655440000" },
219
- });
220
- (0, vitest_1.expect)(postResponse.statusCode).toBe(403);
221
- });
222
- (0, vitest_1.it)("read-only key cannot access api-keys routes", async () => {
223
- (0, test_helpers_js_1.setupReadOnlyApiKey)(stratum);
224
- const response = await app.inject({
225
- method: "GET",
226
- url: "/api/v1/api-keys",
227
- headers: (0, test_helpers_js_1.authHeaders)(),
228
- });
229
- (0, vitest_1.expect)(response.statusCode).toBe(403);
230
- });
231
- (0, vitest_1.it)("returns 401 without authentication", async () => {
232
- const response = await app.inject({
233
- method: "GET",
234
- url: "/api/v1/api-keys",
235
- });
236
- (0, vitest_1.expect)(response.statusCode).toBe(401);
237
- });
238
- });
239
- });
240
- //# sourceMappingURL=api-keys.test.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"api-keys.test.js","sourceRoot":"","sources":["../../src/__tests__/api-keys.test.ts"],"names":[],"mappings":"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"}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=auth.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"auth.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/auth.test.ts"],"names":[],"mappings":""}
@@ -1,229 +0,0 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- const vitest_1 = require("vitest");
7
- const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
8
- const test_helpers_js_1 = require("./test-helpers.js");
9
- (0, vitest_1.describe)("Auth Middleware", () => {
10
- let app;
11
- let stratum;
12
- (0, vitest_1.beforeEach)(async () => {
13
- stratum = (0, test_helpers_js_1.createMockStratum)();
14
- app = await (0, test_helpers_js_1.buildTestApp)(stratum);
15
- });
16
- (0, vitest_1.afterEach)(async () => {
17
- await app.close();
18
- });
19
- // ── API Key Authentication ──────────────────────────────────────────
20
- (0, vitest_1.describe)("API Key Authentication", () => {
21
- (0, vitest_1.it)("valid API key authenticates successfully", async () => {
22
- (0, test_helpers_js_1.setupAdminApiKey)(stratum);
23
- stratum.getTenant.mockResolvedValue(test_helpers_js_1.SAMPLE_TENANT);
24
- const response = await app.inject({
25
- method: "GET",
26
- url: `/api/v1/tenants/${test_helpers_js_1.SAMPLE_TENANT.id}`,
27
- headers: (0, test_helpers_js_1.authHeaders)(),
28
- });
29
- (0, vitest_1.expect)(response.statusCode).toBe(200);
30
- (0, vitest_1.expect)(stratum.validateApiKey).toHaveBeenCalledWith("sk_test_valid_key");
31
- });
32
- (0, vitest_1.it)("invalid API key returns 401", async () => {
33
- stratum.validateApiKey.mockResolvedValue(null);
34
- const response = await app.inject({
35
- method: "GET",
36
- url: `/api/v1/tenants/${test_helpers_js_1.SAMPLE_TENANT.id}`,
37
- headers: { "x-api-key": "sk_test_bad_key" },
38
- });
39
- (0, vitest_1.expect)(response.statusCode).toBe(401);
40
- const body = response.json();
41
- (0, vitest_1.expect)(body.error.code).toBe("UNAUTHORIZED");
42
- (0, vitest_1.expect)(body.error.message).toContain("Invalid or revoked");
43
- });
44
- (0, vitest_1.it)("expired API key returns 401 (validateApiKey returns null)", async () => {
45
- // An expired key causes validateApiKey to return null
46
- stratum.validateApiKey.mockResolvedValue(null);
47
- const response = await app.inject({
48
- method: "GET",
49
- url: `/api/v1/tenants/${test_helpers_js_1.SAMPLE_TENANT.id}`,
50
- headers: { "x-api-key": "sk_test_expired_key" },
51
- });
52
- (0, vitest_1.expect)(response.statusCode).toBe(401);
53
- });
54
- });
55
- // ── JWT Authentication ──────────────────────────────────────────────
56
- (0, vitest_1.describe)("JWT Authentication", () => {
57
- (0, vitest_1.it)("valid JWT authenticates successfully", async () => {
58
- stratum.getTenant.mockResolvedValue(test_helpers_js_1.SAMPLE_TENANT);
59
- const response = await app.inject({
60
- method: "GET",
61
- url: `/api/v1/tenants/${test_helpers_js_1.SAMPLE_TENANT.id}`,
62
- headers: (0, test_helpers_js_1.jwtHeaders)({ scopes: ["read", "write", "admin"] }),
63
- });
64
- (0, vitest_1.expect)(response.statusCode).toBe(200);
65
- // validateApiKey should NOT be called for JWT auth
66
- (0, vitest_1.expect)(stratum.validateApiKey).not.toHaveBeenCalled();
67
- });
68
- (0, vitest_1.it)("invalid JWT returns 401", async () => {
69
- const response = await app.inject({
70
- method: "GET",
71
- url: `/api/v1/tenants/${test_helpers_js_1.SAMPLE_TENANT.id}`,
72
- headers: { authorization: "Bearer invalid.token.here" },
73
- });
74
- (0, vitest_1.expect)(response.statusCode).toBe(401);
75
- const body = response.json();
76
- (0, vitest_1.expect)(body.error.code).toBe("UNAUTHORIZED");
77
- (0, vitest_1.expect)(body.error.message).toContain("Invalid or expired");
78
- });
79
- (0, vitest_1.it)("expired JWT returns 401", async () => {
80
- const expiredToken = jsonwebtoken_1.default.sign({ sub: "test-user", scopes: ["read"] }, test_helpers_js_1.TEST_JWT_SECRET, { algorithm: "HS256", expiresIn: "-1h" });
81
- const response = await app.inject({
82
- method: "GET",
83
- url: `/api/v1/tenants/${test_helpers_js_1.SAMPLE_TENANT.id}`,
84
- headers: { authorization: `Bearer ${expiredToken}` },
85
- });
86
- (0, vitest_1.expect)(response.statusCode).toBe(401);
87
- });
88
- (0, vitest_1.it)("JWT signed with wrong secret returns 401", async () => {
89
- const response = await app.inject({
90
- method: "GET",
91
- url: `/api/v1/tenants/${test_helpers_js_1.SAMPLE_TENANT.id}`,
92
- headers: (0, test_helpers_js_1.jwtHeaders)({}, "wrong-secret-key"),
93
- });
94
- (0, vitest_1.expect)(response.statusCode).toBe(401);
95
- });
96
- });
97
- // ── No Auth ─────────────────────────────────────────────────────────
98
- (0, vitest_1.describe)("No Auth", () => {
99
- (0, vitest_1.it)("returns 401 when no auth header is provided", async () => {
100
- const response = await app.inject({
101
- method: "GET",
102
- url: `/api/v1/tenants/${test_helpers_js_1.SAMPLE_TENANT.id}`,
103
- });
104
- (0, vitest_1.expect)(response.statusCode).toBe(401);
105
- const body = response.json();
106
- (0, vitest_1.expect)(body.error.code).toBe("UNAUTHORIZED");
107
- (0, vitest_1.expect)(body.error.message).toContain("Authentication required");
108
- });
109
- });
110
- // ── Exempt endpoints ────────────────────────────────────────────────
111
- (0, vitest_1.describe)("Exempt endpoints", () => {
112
- (0, vitest_1.it)("health endpoint skips auth", async () => {
113
- const response = await app.inject({
114
- method: "GET",
115
- url: "/api/v1/health",
116
- });
117
- // Health endpoint tries to query the DB, which will fail in test,
118
- // but it should NOT return 401
119
- (0, vitest_1.expect)(response.statusCode).not.toBe(401);
120
- // The response should be 200 (health route handles db errors gracefully)
121
- (0, vitest_1.expect)(response.statusCode).toBe(200);
122
- const body = response.json();
123
- (0, vitest_1.expect)(body.status).toBe("ok");
124
- });
125
- (0, vitest_1.it)("swagger docs endpoint skips auth", async () => {
126
- // We don't register swagger in tests, so the route won't exist,
127
- // but the auth middleware should NOT fire for /api/docs paths.
128
- // We can verify by testing that a 404 is returned (not a 401).
129
- const response = await app.inject({
130
- method: "GET",
131
- url: "/api/docs/json",
132
- });
133
- // Should be 404 (not registered in test app) rather than 401
134
- (0, vitest_1.expect)(response.statusCode).not.toBe(401);
135
- });
136
- });
137
- // ── Authorization scope enforcement ─────────────────────────────────
138
- (0, vitest_1.describe)("Scope enforcement", () => {
139
- (0, vitest_1.it)("read-scope key cannot access write endpoints", async () => {
140
- (0, test_helpers_js_1.setupReadOnlyApiKey)(stratum);
141
- const response = await app.inject({
142
- method: "POST",
143
- url: "/api/v1/tenants",
144
- headers: (0, test_helpers_js_1.authHeaders)(),
145
- payload: { slug: "test", name: "Test" },
146
- });
147
- (0, vitest_1.expect)(response.statusCode).toBe(403);
148
- (0, vitest_1.expect)(response.json().error.code).toBe("FORBIDDEN");
149
- });
150
- (0, vitest_1.it)("read-scope key cannot DELETE", async () => {
151
- (0, test_helpers_js_1.setupReadOnlyApiKey)(stratum);
152
- const response = await app.inject({
153
- method: "DELETE",
154
- url: `/api/v1/tenants/${test_helpers_js_1.SAMPLE_TENANT.id}`,
155
- headers: (0, test_helpers_js_1.authHeaders)(),
156
- });
157
- (0, vitest_1.expect)(response.statusCode).toBe(403);
158
- });
159
- (0, vitest_1.it)("read-scope key cannot PATCH", async () => {
160
- (0, test_helpers_js_1.setupReadOnlyApiKey)(stratum);
161
- const response = await app.inject({
162
- method: "PATCH",
163
- url: `/api/v1/tenants/${test_helpers_js_1.SAMPLE_TENANT.id}`,
164
- headers: (0, test_helpers_js_1.authHeaders)(),
165
- payload: { name: "Updated" },
166
- });
167
- (0, vitest_1.expect)(response.statusCode).toBe(403);
168
- });
169
- (0, vitest_1.it)("admin-scope key can access all endpoints", async () => {
170
- (0, test_helpers_js_1.setupAdminApiKey)(stratum);
171
- stratum.createTenant.mockResolvedValue(test_helpers_js_1.SAMPLE_TENANT);
172
- stratum.getTenant.mockResolvedValue(test_helpers_js_1.SAMPLE_TENANT);
173
- stratum.listApiKeys.mockResolvedValue([]);
174
- // Test write endpoint
175
- const postResponse = await app.inject({
176
- method: "POST",
177
- url: "/api/v1/tenants",
178
- headers: (0, test_helpers_js_1.authHeaders)(),
179
- payload: { slug: "test", name: "Test" },
180
- });
181
- (0, vitest_1.expect)(postResponse.statusCode).toBe(201);
182
- // Test read endpoint
183
- const getResponse = await app.inject({
184
- method: "GET",
185
- url: `/api/v1/tenants/${test_helpers_js_1.SAMPLE_TENANT.id}`,
186
- headers: (0, test_helpers_js_1.authHeaders)(),
187
- });
188
- (0, vitest_1.expect)(getResponse.statusCode).toBe(200);
189
- // Test admin-only endpoint (api-keys)
190
- const adminResponse = await app.inject({
191
- method: "GET",
192
- url: "/api/v1/api-keys",
193
- headers: (0, test_helpers_js_1.authHeaders)(),
194
- });
195
- (0, vitest_1.expect)(adminResponse.statusCode).toBe(200);
196
- });
197
- (0, vitest_1.it)("write-scope key cannot access admin-only endpoints (api-keys)", async () => {
198
- (0, test_helpers_js_1.setupWriteApiKey)(stratum);
199
- const response = await app.inject({
200
- method: "GET",
201
- url: "/api/v1/api-keys",
202
- headers: (0, test_helpers_js_1.authHeaders)(),
203
- });
204
- (0, vitest_1.expect)(response.statusCode).toBe(403);
205
- (0, vitest_1.expect)(response.json().error.code).toBe("FORBIDDEN");
206
- });
207
- (0, vitest_1.it)("JWT with read-only scopes cannot POST", async () => {
208
- stratum.getTenant.mockResolvedValue(test_helpers_js_1.SAMPLE_TENANT);
209
- const response = await app.inject({
210
- method: "POST",
211
- url: "/api/v1/tenants",
212
- headers: (0, test_helpers_js_1.jwtHeaders)({ scopes: ["read"] }),
213
- payload: { slug: "test", name: "Test" },
214
- });
215
- (0, vitest_1.expect)(response.statusCode).toBe(403);
216
- });
217
- (0, vitest_1.it)("JWT with admin scopes can access admin-only endpoints", async () => {
218
- stratum.getTenant.mockResolvedValue(test_helpers_js_1.SAMPLE_TENANT);
219
- stratum.listApiKeys.mockResolvedValue([]);
220
- const response = await app.inject({
221
- method: "GET",
222
- url: "/api/v1/api-keys",
223
- headers: (0, test_helpers_js_1.jwtHeaders)({ scopes: ["read", "write", "admin"] }),
224
- });
225
- (0, vitest_1.expect)(response.statusCode).toBe(200);
226
- });
227
- });
228
- });
229
- //# sourceMappingURL=auth.test.js.map