@endora-commerce/mod-api-keys 0.100.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 (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +57 -0
  3. package/dist/admin/index.d.ts +37 -0
  4. package/dist/admin/index.d.ts.map +1 -0
  5. package/dist/admin/index.js +43 -0
  6. package/dist/admin/index.js.map +1 -0
  7. package/dist/admin/pages/ApiKeysPage.d.ts +3 -0
  8. package/dist/admin/pages/ApiKeysPage.d.ts.map +1 -0
  9. package/dist/admin/pages/ApiKeysPage.js +182 -0
  10. package/dist/admin/pages/ApiKeysPage.js.map +1 -0
  11. package/dist/backend/entities/api-key.entity.d.ts +33 -0
  12. package/dist/backend/entities/api-key.entity.d.ts.map +1 -0
  13. package/dist/backend/entities/api-key.entity.js +118 -0
  14. package/dist/backend/entities/api-key.entity.js.map +1 -0
  15. package/dist/backend/index.d.ts +71 -0
  16. package/dist/backend/index.d.ts.map +1 -0
  17. package/dist/backend/index.js +57 -0
  18. package/dist/backend/index.js.map +1 -0
  19. package/dist/backend/plugin.d.ts +65 -0
  20. package/dist/backend/plugin.d.ts.map +1 -0
  21. package/dist/backend/plugin.js +75 -0
  22. package/dist/backend/plugin.js.map +1 -0
  23. package/dist/backend/routes.d.ts +26 -0
  24. package/dist/backend/routes.d.ts.map +1 -0
  25. package/dist/backend/routes.js +51 -0
  26. package/dist/backend/routes.js.map +1 -0
  27. package/dist/backend/services/api-key-service.d.ts +72 -0
  28. package/dist/backend/services/api-key-service.d.ts.map +1 -0
  29. package/dist/backend/services/api-key-service.js +186 -0
  30. package/dist/backend/services/api-key-service.js.map +1 -0
  31. package/dist/manifest.d.ts +169 -0
  32. package/dist/manifest.d.ts.map +1 -0
  33. package/dist/manifest.js +155 -0
  34. package/dist/manifest.js.map +1 -0
  35. package/dist/migrations/20260724T173916_api_keys_distributor_binding.d.ts +50 -0
  36. package/dist/migrations/20260724T173916_api_keys_distributor_binding.d.ts.map +1 -0
  37. package/dist/migrations/20260724T173916_api_keys_distributor_binding.js +117 -0
  38. package/dist/migrations/20260724T173916_api_keys_distributor_binding.js.map +1 -0
  39. package/dist/migrations/index.d.ts +27 -0
  40. package/dist/migrations/index.d.ts.map +1 -0
  41. package/dist/migrations/index.js +29 -0
  42. package/dist/migrations/index.js.map +1 -0
  43. package/docs/api_keys.md +89 -0
  44. package/i18n/en.json +5 -0
  45. package/i18n/pl.json +5 -0
  46. package/package.json +93 -0
  47. package/tailwind.css +14 -0
@@ -0,0 +1,57 @@
1
+ import { lazyPort } from '@endora-commerce/platform/kernel';
2
+ import { integrationsModule } from './plugin.js';
3
+ import { ApiKey } from './entities/api-key.entity.js';
4
+ export function registerModule(ctx) {
5
+ ctx.di.register({
6
+ // One registration holds the whole graph: the two gates close over the
7
+ // same `resolveScopedKey`, so building them separately would mean two
8
+ // authenticators disagreeing about what a scope means.
9
+ apiKeys: ctx
10
+ .asFunction(({ emFactory, auditLogService }) => integrationsModule({
11
+ emFactory,
12
+ auditLogService,
13
+ // Resolved per check rather than captured: `requireAdmin` is a
14
+ // transient port gate, and a captured one keeps admitting requests
15
+ // after `auth` is switched off.
16
+ requireAdmin: (permission) => async (req, reply) => ctx.cradle().requireAdmin(permission)(req, reply),
17
+ resolveAdminUserId: (request) => ctx.cradle().adminContextResolver(request).adminUserId,
18
+ // Feature 075 (D-87) — the two owners the binding rules ask about.
19
+ // Both were `em.getKnex().raw('select … from "organizations"')` and
20
+ // `… from "customer_accounts"` inside the service: raw SQL naming
21
+ // no specifier, so the boundary compiled and returned rows. Gated
22
+ // ports, so a singleton may not hold one directly.
23
+ bindingPorts: {
24
+ organizations: lazyPort(ctx, 'organizationDetailsPort'),
25
+ customerAccounts: lazyPort(ctx, 'customerAccountReadPort'),
26
+ },
27
+ }))
28
+ .singleton(),
29
+ });
30
+ ctx.di.providePort('apiKeyService', ctx.asFunction(({ apiKeys }) => apiKeys.handle.apiKeyService).singleton());
31
+ ctx.di.providePort('requireApiKey', ctx.asFunction(({ apiKeys }) => apiKeys.handle.requireApiKey).singleton());
32
+ ctx.di.providePort('requireBoundApiKey', ctx.asFunction(({ apiKeys }) => apiKeys.handle.requireBoundApiKey).singleton());
33
+ ctx.di.providePort('apiKeyResolver', ctx
34
+ .asFunction(({ apiKeys }) => async (token) => apiKeys.handle.apiKeyService.authenticate(token))
35
+ .singleton());
36
+ ctx.routes(async (app) => {
37
+ // The factory's own plugin, not a re-registration: it mounts the admin
38
+ // routes and nothing else, and duplicating that here would be a second
39
+ // place to keep in step.
40
+ await ctx.cradle().apiKeys.plugin(app);
41
+ });
42
+ }
43
+ /**
44
+ * The module's persisted entity classes, on the `./backend` subpath, as one
45
+ * array and **no named class export** (D-168).
46
+ *
47
+ * This is the shape the platform reads when the package is *installed*: the
48
+ * boot-time loader (`src/packages/package-runtime.ts`, `exported['entities']`)
49
+ * and the static declaration reader (`scripts/lib/package-declarations.ts`),
50
+ * which is the third source of `check:module-boundary`'s `table→owner` map and
51
+ * the package pass of `check-entity-tenant-classification`. A missing array is
52
+ * answered with `[]` — zero entities registered, no error anywhere.
53
+ */
54
+ export const entities = [
55
+ ApiKey,
56
+ ];
57
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/backend/index.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,QAAQ,EAAE,MAAM,kCAAkC,CAAC;AAG5D,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EAAE,MAAM,EAAE,MAAM,8BAA8B,CAAC;AAqDtD,MAAM,UAAU,cAAc,CAAC,GAAkB;IAC/C,GAAG,CAAC,EAAE,CAAC,QAAQ,CAAC;QACd,uEAAuE;QACvE,sEAAsE;QACtE,uDAAuD;QACvD,OAAO,EAAE,GAAG;aACT,UAAU,CACT,CAAC,EAAE,SAAS,EAAE,eAAe,EAAiB,EAAE,EAAE,CAChD,kBAAkB,CAAC;YACjB,SAAS;YACT,eAAe;YACf,+DAA+D;YAC/D,mEAAmE;YACnE,gCAAgC;YAChC,YAAY,EAAE,CAAC,UAAU,EAAE,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,EAAE,CACjD,GAAG,CAAC,MAAM,EAAiB,CAAC,YAAY,CAAC,UAAU,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC;YAClE,kBAAkB,EAAE,CAAC,OAAO,EAAE,EAAE,CAC9B,GAAG,CAAC,MAAM,EAAiB,CAAC,oBAAoB,CAAC,OAAO,CAAC,CAAC,WAAW;YACvE,mEAAmE;YACnE,oEAAoE;YACpE,kEAAkE;YAClE,kEAAkE;YAClE,mDAAmD;YACnD,YAAY,EAAE;gBACZ,aAAa,EAAE,QAAQ,CAA0B,GAAG,EAAE,yBAAyB,CAAC;gBAChF,gBAAgB,EAAE,QAAQ,CACxB,GAAG,EACH,yBAAyB,CAC1B;aACF;SACF,CAAC,CACL;aACA,SAAS,EAAE;KACf,CAAC,CAAC;IAEH,GAAG,CAAC,EAAE,CAAC,WAAW,CAChB,eAAe,EACf,GAAG,CAAC,UAAU,CAAC,CAAC,EAAE,OAAO,EAAiB,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,SAAS,EAAE,CACzF,CAAC;IAEF,GAAG,CAAC,EAAE,CAAC,WAAW,CAChB,eAAe,EACf,GAAG,CAAC,UAAU,CAAC,CAAC,EAAE,OAAO,EAAiB,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,SAAS,EAAE,CACzF,CAAC;IAEF,GAAG,CAAC,EAAE,CAAC,WAAW,CAChB,oBAAoB,EACpB,GAAG,CAAC,UAAU,CAAC,CAAC,EAAE,OAAO,EAAiB,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,kBAAkB,CAAC,CAAC,SAAS,EAAE,CAC9F,CAAC;IAEF,GAAG,CAAC,EAAE,CAAC,WAAW,CAChB,gBAAgB,EAChB,GAAG;SACA,UAAU,CACT,CAAC,EAAE,OAAO,EAAiB,EAAE,EAAE,CAC7B,KAAK,EAAE,KAAa,EAAE,EAAE,CACtB,OAAO,CAAC,MAAM,CAAC,aAAa,CAAC,YAAY,CAAC,KAAK,CAAC,CACrD;SACA,SAAS,EAAE,CACf,CAAC;IAEF,GAAG,CAAC,MAAM,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE;QACvB,uEAAuE;QACvE,uEAAuE;QACvE,yBAAyB;QACzB,MAAM,GAAG,CAAC,MAAM,EAAiB,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IACxD,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG;IACtB,MAAM;CACP,CAAC"}
@@ -0,0 +1,65 @@
1
+ import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
2
+ import type { EntityManager } from '@mikro-orm/postgresql';
3
+ import { ApiKeyService, type ApiKeyBindingPorts } from './services/api-key-service.js';
4
+ import type { AuditPort } from '@endora-commerce/platform/kernel';
5
+ import type { RequireAdminFactory } from '@endora-commerce/platform/kernel';
6
+ /**
7
+ * Composition root for the US7 surface — API keys + webhooks.
8
+ * Returns a handle so other modules (e.g. catalog routes.api-key.ts) can
9
+ * consume the api-key gate this builds.
10
+ */
11
+ export interface IntegrationsModuleOptions {
12
+ emFactory: () => EntityManager;
13
+ requireAdmin: RequireAdminFactory;
14
+ auditLogService: AuditPort;
15
+ /**
16
+ * `organizations`' and `customer_accounts`' published reads, which the
17
+ * binding rules B3 and B4 decide on (feature 075, D-87). Both were raw SQL
18
+ * against those modules' tables inside the service.
19
+ */
20
+ bindingPorts: ApiKeyBindingPorts;
21
+ /**
22
+ * Who the acting admin is, from the production actor (feature 080, T051).
23
+ * Threaded through to the admin routes, which used to read
24
+ * `request.testActor` — see `ApiKeysAdminDeps.resolveAdminUserId`.
25
+ */
26
+ resolveAdminUserId: (request: FastifyRequest) => string;
27
+ }
28
+ /**
29
+ * Feature 062 — the resolved binding `requireBoundApiKey` stashes on the
30
+ * request for the distributor services.
31
+ */
32
+ export interface ApiKeyRequestBinding {
33
+ apiKeyId: string;
34
+ scopes: string[];
35
+ organizationId: string;
36
+ salesChannelId: string;
37
+ customerAccountId: string;
38
+ }
39
+ declare module 'fastify' {
40
+ interface FastifyRequest {
41
+ /** Set by `requireBoundApiKey` after a successful binding assertion. */
42
+ apiKeyBinding?: ApiKeyRequestBinding;
43
+ }
44
+ }
45
+ export interface IntegrationsModuleHandle {
46
+ apiKeyService: ApiKeyService;
47
+ /**
48
+ * Pre-handler factory that authenticates a Bearer token via ApiKeyService
49
+ * and gates on the requested scope. Used by api-key route surfaces (e.g.
50
+ * the catalog by-sku upsert in routes.api-key.ts) — out-of-scope returns
51
+ * 403 + an audit row tagging the api key (T220).
52
+ */
53
+ requireApiKey: (scope: string) => (req: FastifyRequest, reply: FastifyReply) => Promise<void>;
54
+ /**
55
+ * Feature 062 — distributor gate: authenticate → scope (audited 403,
56
+ * unchanged) → binding assertion (403 `API_KEY_NOT_BOUND` + audit
57
+ * `api_key.not_bound`) → stash the resolved binding on the request.
58
+ */
59
+ requireBoundApiKey: (scope: string) => (req: FastifyRequest, reply: FastifyReply) => Promise<void>;
60
+ }
61
+ export declare function integrationsModule(options: IntegrationsModuleOptions): {
62
+ plugin: (app: FastifyInstance) => Promise<void>;
63
+ handle: IntegrationsModuleHandle;
64
+ };
65
+ //# sourceMappingURL=plugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../../src/backend/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAC7E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAG3D,OAAO,EACL,aAAa,EACb,KAAK,kBAAkB,EAExB,MAAM,+BAA+B,CAAC;AAEvC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kCAAkC,CAAC;AAClE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AAE5E;;;;GAIG;AAEH,MAAM,WAAW,yBAAyB;IACxC,SAAS,EAAE,MAAM,aAAa,CAAC;IAC/B,YAAY,EAAE,mBAAmB,CAAC;IAClC,eAAe,EAAE,SAAS,CAAC;IAC3B;;;;OAIG;IACH,YAAY,EAAE,kBAAkB,CAAC;IACjC;;;;OAIG;IACH,kBAAkB,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,MAAM,CAAC;CACzD;AAED;;;GAGG;AACH,MAAM,WAAW,oBAAoB;IACnC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,cAAc,EAAE,MAAM,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;IACvB,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAED,OAAO,QAAQ,SAAS,CAAC;IACvB,UAAU,cAAc;QACtB,wEAAwE;QACxE,aAAa,CAAC,EAAE,oBAAoB,CAAC;KACtC;CACF;AAED,MAAM,WAAW,wBAAwB;IACvC,aAAa,EAAE,aAAa,CAAC;IAC7B;;;;;OAKG;IACH,aAAa,EAAE,CACb,KAAK,EAAE,MAAM,KACV,CAAC,GAAG,EAAE,cAAc,EAAE,KAAK,EAAE,YAAY,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACjE;;;;OAIG;IACH,kBAAkB,EAAE,CAClB,KAAK,EAAE,MAAM,KACV,CAAC,GAAG,EAAE,cAAc,EAAE,KAAK,EAAE,YAAY,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAClE;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,yBAAyB,GAAG;IACtE,MAAM,EAAE,CAAC,GAAG,EAAE,eAAe,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAChD,MAAM,EAAE,wBAAwB,CAAC;CAClC,CA4FA"}
@@ -0,0 +1,75 @@
1
+ import { ERROR_CODES } from '@endora-commerce/contracts';
2
+ import { HttpError } from '@endora-commerce/platform/http';
3
+ import { ApiKeyService, } from './services/api-key-service.js';
4
+ import { registerApiKeysAdminRoutes } from './routes.js';
5
+ export function integrationsModule(options) {
6
+ const apiKeyService = new ApiKeyService(options.emFactory, options.bindingPorts, options.auditLogService);
7
+ // Shared authenticate + scope assertion used by both gates.
8
+ const resolveScopedKey = async (request, scope) => {
9
+ const header = request.headers.authorization;
10
+ if (!header?.startsWith('Bearer ')) {
11
+ throw new HttpError(401, ERROR_CODES.UNAUTHORIZED, 'API key required.');
12
+ }
13
+ const token = header.slice('Bearer '.length).trim();
14
+ const resolved = await apiKeyService.authenticate(token);
15
+ if (!resolved) {
16
+ throw new HttpError(401, ERROR_CODES.UNAUTHORIZED, 'API key is invalid or revoked.');
17
+ }
18
+ if (!resolved.scopes.includes(scope)) {
19
+ if (options.auditLogService) {
20
+ await options.auditLogService.record({
21
+ action: 'api_key.out_of_scope',
22
+ objectType: 'api_key',
23
+ objectId: resolved.apiKeyId,
24
+ stateBefore: null,
25
+ stateAfter: { attemptedScope: scope, grantedScopes: resolved.scopes },
26
+ });
27
+ }
28
+ throw new HttpError(403, ERROR_CODES.API_KEY_OUT_OF_SCOPE, `API key lacks the required scope: ${scope}.`);
29
+ }
30
+ return resolved;
31
+ };
32
+ const requireApiKey = (scope) => async (request) => {
33
+ await resolveScopedKey(request, scope);
34
+ };
35
+ // Feature 062 — distributor gate (contracts/api-key-binding.md §4).
36
+ const requireBoundApiKey = (scope) => async (request) => {
37
+ const resolved = await resolveScopedKey(request, scope);
38
+ if (!resolved.organizationId ||
39
+ !resolved.salesChannelId ||
40
+ !resolved.customerAccountId) {
41
+ if (options.auditLogService) {
42
+ await options.auditLogService.record({
43
+ action: 'api_key.not_bound',
44
+ objectType: 'api_key',
45
+ objectId: resolved.apiKeyId,
46
+ stateBefore: null,
47
+ stateAfter: { attemptedScope: scope },
48
+ });
49
+ }
50
+ throw new HttpError(403, ERROR_CODES.API_KEY_NOT_BOUND, 'This endpoint requires a distributor-bound API key.');
51
+ }
52
+ request.apiKeyBinding = {
53
+ apiKeyId: resolved.apiKeyId,
54
+ scopes: resolved.scopes,
55
+ organizationId: resolved.organizationId,
56
+ salesChannelId: resolved.salesChannelId,
57
+ customerAccountId: resolved.customerAccountId,
58
+ };
59
+ };
60
+ return {
61
+ handle: {
62
+ apiKeyService,
63
+ requireApiKey,
64
+ requireBoundApiKey,
65
+ },
66
+ plugin: async (app) => {
67
+ await registerApiKeysAdminRoutes(app, {
68
+ apiKeyService,
69
+ requireAdmin: options.requireAdmin,
70
+ resolveAdminUserId: options.resolveAdminUserId,
71
+ });
72
+ },
73
+ };
74
+ }
75
+ //# sourceMappingURL=plugin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.js","sourceRoot":"","sources":["../../src/backend/plugin.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,26 @@
1
+ import type { FastifyInstance, FastifyRequest } from 'fastify';
2
+ import type { ApiKeyService } from './services/api-key-service.js';
3
+ import type { RequireAdminFactory } from '@endora-commerce/platform/kernel';
4
+ export interface ApiKeysAdminDeps {
5
+ apiKeyService: ApiKeyService;
6
+ requireAdmin: RequireAdminFactory;
7
+ /**
8
+ * The acting admin's id, resolved from the request the guard in front of
9
+ * these routes has already accepted.
10
+ *
11
+ * Injected rather than read here, because the read used to be
12
+ * `testAdminUserId(request)` — `http/test-actor-carrier`, which
13
+ * `contracts/host-package.md` §1.4j classifies **A**: the file exists to
14
+ * narrow this repository's test-harness Fastify augmentation, and an
15
+ * installed package has no relationship to that harness. It also answered
16
+ * `undefined` in production for every request, because nothing under `src/`
17
+ * writes `request.testActor`.
18
+ *
19
+ * Both composition roots supply it from `adminContextResolver`, which reads
20
+ * the production actor and throws 401 for a non-admin. Every call site sits
21
+ * behind `requireAdmin(...)`, so the actor is an admin by the time it runs.
22
+ */
23
+ resolveAdminUserId: (request: FastifyRequest) => string;
24
+ }
25
+ export declare function registerApiKeysAdminRoutes(app: FastifyInstance, deps: ApiKeysAdminDeps): Promise<void>;
26
+ //# sourceMappingURL=routes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/backend/routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAE/D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAEnE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AAE5E,MAAM,WAAW,gBAAgB;IAC/B,aAAa,EAAE,aAAa,CAAC;IAC7B,YAAY,EAAE,mBAAmB,CAAC;IAClC;;;;;;;;;;;;;;;OAeG;IACH,kBAAkB,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,MAAM,CAAC;CACzD;AAED,wBAAsB,0BAA0B,CAC9C,GAAG,EAAE,eAAe,EACpB,IAAI,EAAE,gBAAgB,GACrB,OAAO,CAAC,IAAI,CAAC,CA6Cf"}
@@ -0,0 +1,51 @@
1
+ import { createApiKeyRequestSchema } from '@endora-commerce/contracts';
2
+ export async function registerApiKeysAdminRoutes(app, deps) {
3
+ const { apiKeyService, requireAdmin, resolveAdminUserId } = deps;
4
+ app.get('/api/v1/admin/api-keys', { preHandler: requireAdmin('integrations:manage') }, async () => {
5
+ const rows = await apiKeyService.list();
6
+ return { data: rows.map(serializeApiKey) };
7
+ });
8
+ app.post('/api/v1/admin/api-keys', {
9
+ preHandler: requireAdmin('integrations:manage'),
10
+ schema: { body: createApiKeyRequestSchema },
11
+ }, async (request, reply) => {
12
+ const body = createApiKeyRequestSchema.parse(request.body);
13
+ const result = await apiKeyService.create({
14
+ name: body.name,
15
+ scopes: body.scopes,
16
+ ...(body.binding !== undefined ? { binding: body.binding } : {}),
17
+ ...(body.expiresAt !== undefined ? { expiresAt: new Date(body.expiresAt) } : {}),
18
+ createdByAdminUserId: resolveAdminUserId(request),
19
+ });
20
+ reply.status(201);
21
+ return {
22
+ data: {
23
+ apiKey: serializeApiKey(result.apiKey),
24
+ bearerToken: result.bearerToken,
25
+ },
26
+ };
27
+ });
28
+ app.delete('/api/v1/admin/api-keys/:id', { preHandler: requireAdmin('integrations:manage') }, async (request, reply) => {
29
+ await apiKeyService.revoke(request.params.id);
30
+ return reply.status(204).send();
31
+ });
32
+ }
33
+ function serializeApiKey(a) {
34
+ return {
35
+ id: a.id,
36
+ name: a.name,
37
+ lastFour: a.lastFour,
38
+ scopes: a.scopes,
39
+ status: a.status,
40
+ lastUsedAt: a.lastUsedAt?.toISOString() ?? null,
41
+ revokedAt: a.revokedAt?.toISOString() ?? null,
42
+ createdAt: a.createdAt.toISOString(),
43
+ updatedAt: a.updatedAt.toISOString(),
44
+ // Feature 062 — additive binding/expiry fields (null for unbound keys).
45
+ organizationId: a.organizationId ?? null,
46
+ salesChannelId: a.salesChannelId ?? null,
47
+ customerAccountId: a.customerAccountId ?? null,
48
+ expiresAt: a.expiresAt?.toISOString() ?? null,
49
+ };
50
+ }
51
+ //# sourceMappingURL=routes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.js","sourceRoot":"","sources":["../../src/backend/routes.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,72 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql';
2
+ import { type ApiKeyBinding, type CustomerAccountReadPort, type OrganizationDetailsPort } from '@endora-commerce/contracts';
3
+ import { ApiKey } from '../entities/api-key.entity.js';
4
+ import type { AuditPort } from '@endora-commerce/platform/kernel';
5
+ /**
6
+ * ApiKeyService (T227, extended by feature 062 — distributor binding).
7
+ *
8
+ * - create: generates a `sk_live_<random>` token, stores its sha256 hash, and
9
+ * returns the raw token + ApiKey row. Caller must show the token once.
10
+ * Validates the distributor binding rules B1–B5
11
+ * (specs/062-distributor-api/contracts/api-key-binding.md §2).
12
+ * - revoke: marks status='revoked', sets revokedAt.
13
+ * - authenticate: hashes the supplied bearer, looks up by hash, validates
14
+ * status + expiry. Returns the resolved identity (incl. binding) or null.
15
+ * Used by the api-key auth gates and the ambient actor resolver.
16
+ */
17
+ export interface CreateApiKeyResult {
18
+ apiKey: ApiKey;
19
+ /** Raw bearer token. Returned exactly once. */
20
+ bearerToken: string;
21
+ }
22
+ /** Resolved identity of an authenticated key, incl. the distributor binding. */
23
+ export interface AuthenticatedApiKey {
24
+ apiKeyId: string;
25
+ scopes: string[];
26
+ organizationId: string | null;
27
+ salesChannelId: string | null;
28
+ customerAccountId: string | null;
29
+ }
30
+ /**
31
+ * The two owners this module asks about a binding it is being handed
32
+ * (feature 075, D-87).
33
+ *
34
+ * Both reads were `em.getKnex().raw('select … from "organizations" / …
35
+ * "customer_accounts" …')`. Raw SQL names no import specifier, so those two
36
+ * boundary crossings compiled, returned rows and were invisible to the import
37
+ * predicate until feature 077 taught the check to read table identifiers.
38
+ *
39
+ * Nothing about the isolation changes by resolving them as ports: `getKnex()`
40
+ * is `getConnection().getKnex()`, which takes its own pooled connection, so
41
+ * neither read could ever see the enclosing transaction's writes either. What
42
+ * changes is that an absent owner now refuses instead of answering — B3 and B4
43
+ * decide whether a distributor key may be minted, and a validation that
44
+ * silently found no organisation because the module holding organisations was
45
+ * gone would mint nothing and blame the operator's input.
46
+ */
47
+ export interface ApiKeyBindingPorts {
48
+ organizations: OrganizationDetailsPort;
49
+ customerAccounts: CustomerAccountReadPort;
50
+ }
51
+ export declare class ApiKeyService {
52
+ #private;
53
+ private readonly emFactory;
54
+ private readonly bindingPorts;
55
+ private readonly auditLog?;
56
+ constructor(emFactory: () => EntityManager, bindingPorts: ApiKeyBindingPorts, auditLog?: AuditPort | undefined);
57
+ create(input: {
58
+ name: string;
59
+ scopes: string[];
60
+ binding?: ApiKeyBinding;
61
+ expiresAt?: Date;
62
+ createdByAdminUserId?: string;
63
+ }): Promise<CreateApiKeyResult>;
64
+ list(): Promise<ApiKey[]>;
65
+ revoke(apiKeyId: string): Promise<void>;
66
+ /**
67
+ * Resolve a raw bearer token to the key's identity. Returns null if
68
+ * invalid / revoked / expired. Updates `lastUsedAt` on success.
69
+ */
70
+ authenticate(rawBearer: string): Promise<AuthenticatedApiKey | null>;
71
+ }
72
+ //# sourceMappingURL=api-key-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api-key-service.d.ts","sourceRoot":"","sources":["../../../src/backend/services/api-key-service.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EAEL,KAAK,aAAa,EAClB,KAAK,uBAAuB,EAC5B,KAAK,uBAAuB,EAC7B,MAAM,4BAA4B,CAAC;AAEpC,OAAO,EAAE,MAAM,EAAE,MAAM,+BAA+B,CAAC;AAGvD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kCAAkC,CAAC;AAElE;;;;;;;;;;;GAWG;AAEH,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,+CAA+C;IAC/C,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,gFAAgF;AAChF,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;CAClC;AAMD;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,WAAW,kBAAkB;IACjC,aAAa,EAAE,uBAAuB,CAAC;IACvC,gBAAgB,EAAE,uBAAuB,CAAC;CAC3C;AAED,qBAAa,aAAa;;IAEtB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,YAAY;IAC7B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC;gBAFT,SAAS,EAAE,MAAM,aAAa,EAC9B,YAAY,EAAE,kBAAkB,EAChC,QAAQ,CAAC,EAAE,SAAS,YAAA;IASjC,MAAM,CAAC,KAAK,EAAE;QAClB,IAAI,EAAE,MAAM,CAAC;QACb,MAAM,EAAE,MAAM,EAAE,CAAC;QACjB,OAAO,CAAC,EAAE,aAAa,CAAC;QACxB,SAAS,CAAC,EAAE,IAAI,CAAC;QACjB,oBAAoB,CAAC,EAAE,MAAM,CAAC;KAC/B,GAAG,OAAO,CAAC,kBAAkB,CAAC;IA0IzB,IAAI,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;IAKzB,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAY7C;;;OAGG;IACG,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,GAAG,IAAI,CAAC;CAoB3E"}
@@ -0,0 +1,186 @@
1
+ import { createHash, randomBytes } from 'crypto';
2
+ import { ERROR_CODES, } from '@endora-commerce/contracts';
3
+ import { HttpError } from '@endora-commerce/platform/http';
4
+ import { ApiKey } from '../entities/api-key.entity.js';
5
+ import { recordAuditFromContext } from '@endora-commerce/platform/commands';
6
+ import { SalesChannel } from '@endora-commerce/platform/kernel';
7
+ const TOKEN_PREFIX = 'sk_live_';
8
+ export class ApiKeyService {
9
+ emFactory;
10
+ bindingPorts;
11
+ auditLog;
12
+ constructor(emFactory, bindingPorts, auditLog) {
13
+ this.emFactory = emFactory;
14
+ this.bindingPorts = bindingPorts;
15
+ this.auditLog = auditLog;
16
+ }
17
+ #audit(em, action, objectId, stateBefore, stateAfter) {
18
+ if (this.auditLog) {
19
+ recordAuditFromContext(this.auditLog, em, { action, objectType: 'api_key', objectId, stateBefore, stateAfter });
20
+ }
21
+ }
22
+ async create(input) {
23
+ const em = this.emFactory();
24
+ await this.#validateCreate(em, input);
25
+ const raw = randomBytes(32).toString('base64url');
26
+ const bearerToken = `${TOKEN_PREFIX}${raw}`;
27
+ const apiKey = em.create(ApiKey, {
28
+ name: input.name,
29
+ keyHash: sha256Hex(bearerToken),
30
+ lastFour: bearerToken.slice(-4),
31
+ scopes: input.scopes,
32
+ ...(input.binding
33
+ ? {
34
+ organizationId: input.binding.organizationId,
35
+ salesChannelId: input.binding.salesChannelId,
36
+ customerAccountId: input.binding.customerAccountId,
37
+ }
38
+ : {}),
39
+ ...(input.expiresAt !== undefined ? { expiresAt: input.expiresAt } : {}),
40
+ ...(input.createdByAdminUserId !== undefined
41
+ ? { createdByAdminUserId: input.createdByAdminUserId }
42
+ : {}),
43
+ });
44
+ em.persist(apiKey);
45
+ this.#audit(em, 'api_key.create', apiKey.id, null, {
46
+ name: apiKey.name,
47
+ scopes: apiKey.scopes,
48
+ binding: input.binding
49
+ ? {
50
+ organizationId: input.binding.organizationId,
51
+ salesChannelId: input.binding.salesChannelId,
52
+ customerAccountId: input.binding.customerAccountId,
53
+ }
54
+ : null,
55
+ expiresAt: input.expiresAt?.toISOString() ?? null,
56
+ });
57
+ await em.flush();
58
+ return { apiKey, bearerToken };
59
+ }
60
+ /**
61
+ * Creation rules B1–B5 (contracts/api-key-binding.md §2). The two foreign
62
+ * rows are read through their owners' published ports (see
63
+ * {@link ApiKeyBindingPorts}); the sales channel is the kernel's own entity
64
+ * and stays an ORM read on the caller's `em`.
65
+ */
66
+ async #validateCreate(em, input) {
67
+ const issues = [];
68
+ const hasOrdersScope = input.scopes.some((s) => s.startsWith('orders:'));
69
+ if (hasOrdersScope && !input.binding) {
70
+ // B1
71
+ issues.push({
72
+ path: 'binding',
73
+ issue: 'Orders scopes require a distributor binding (organization, sales channel, service account).',
74
+ });
75
+ }
76
+ if (input.binding && input.scopes.includes('catalog:write')) {
77
+ // B2
78
+ issues.push({
79
+ path: 'scopes',
80
+ issue: 'catalog:write is not allowed on a bound key — PIM writes stay unbound-only.',
81
+ });
82
+ }
83
+ if (input.binding) {
84
+ const organization = await this.bindingPorts.organizations.findById(input.binding.organizationId);
85
+ // B4. `deletedAt` is checked here rather than assumed of the port: the
86
+ // statement this replaced spelled `and "deleted_at" is null`, and a
87
+ // soft-deleted organisation is a binding target that must not resolve.
88
+ if (organization === null || organization.deletedAt !== null) {
89
+ issues.push({
90
+ path: 'binding.organizationId',
91
+ issue: 'Organization does not exist.',
92
+ });
93
+ }
94
+ /**
95
+ * The kernel's own entity, not `select 1 from "sales_channels"` (feature
96
+ * 075, D-87). `sales_channels` is the kernel's table since feature 072
97
+ * moved the resolution machinery there, and a module relating into the
98
+ * kernel by ORM is the sanctioned access.
99
+ *
100
+ * Read through the caller's `em`, not through
101
+ * `salesChannelResolutionPort.getById`: that accessor forks its own
102
+ * EntityManager, so a channel created earlier in this Command's
103
+ * transaction would not be visible to it and B4 would refuse a binding
104
+ * that is about to be valid.
105
+ */
106
+ const channel = await em.findOne(SalesChannel, { id: input.binding.salesChannelId });
107
+ if (channel === null) {
108
+ // B4 (active not required at creation — the key fails closed while inactive)
109
+ issues.push({
110
+ path: 'binding.salesChannelId',
111
+ issue: 'Sales channel does not exist.',
112
+ });
113
+ }
114
+ // The wider read on purpose: the four columns the statement selected are
115
+ // all on the record, and the three-way "active" predicate below is this
116
+ // module's rule to state, not the port's `activeOnly` shorthand — that
117
+ // one excludes soft-deleted accounts and says nothing about blocked or
118
+ // anonymized ones.
119
+ const account = await this.bindingPorts.customerAccounts.findById(input.binding.customerAccountId);
120
+ const accountActive = account !== null &&
121
+ account.blockedAt === null &&
122
+ account.deletedAt === null &&
123
+ account.anonymizedAt === null;
124
+ if (!account || !accountActive || account.organizationId !== input.binding.organizationId) {
125
+ // B3
126
+ issues.push({
127
+ path: 'binding.customerAccountId',
128
+ issue: 'Service account must be an active customer account of the bound organization.',
129
+ });
130
+ }
131
+ }
132
+ if (input.expiresAt !== undefined && input.expiresAt.getTime() <= Date.now()) {
133
+ // B5
134
+ issues.push({ path: 'expiresAt', issue: 'Expiry must be a future instant.' });
135
+ }
136
+ if (issues.length > 0) {
137
+ throw new HttpError(422, ERROR_CODES.VALIDATION_FAILED, 'API key creation failed binding validation.', issues);
138
+ }
139
+ }
140
+ async list() {
141
+ const em = this.emFactory();
142
+ return em.find(ApiKey, {}, { orderBy: { createdAt: 'desc' } });
143
+ }
144
+ async revoke(apiKeyId) {
145
+ const em = this.emFactory();
146
+ const apiKey = await em.findOne(ApiKey, { id: apiKeyId });
147
+ if (!apiKey) {
148
+ throw new HttpError(404, ERROR_CODES.NOT_FOUND, 'API key not found.');
149
+ }
150
+ apiKey.status = 'revoked';
151
+ apiKey.revokedAt = new Date();
152
+ this.#audit(em, 'api_key.revoke', apiKey.id, { name: apiKey.name }, null);
153
+ await em.flush();
154
+ }
155
+ /**
156
+ * Resolve a raw bearer token to the key's identity. Returns null if
157
+ * invalid / revoked / expired. Updates `lastUsedAt` on success.
158
+ */
159
+ async authenticate(rawBearer) {
160
+ // command-coverage-ignore: stamps last-used — high-volume auth bookkeeping,
161
+ // not an audited domain-state mutation.
162
+ if (!rawBearer.startsWith(TOKEN_PREFIX))
163
+ return null;
164
+ const em = this.emFactory();
165
+ const apiKey = await em.findOne(ApiKey, { keyHash: sha256Hex(rawBearer) });
166
+ if (!apiKey || apiKey.status !== 'active')
167
+ return null;
168
+ // Feature 062 (B5 runtime half) — an expired key is indistinguishable from
169
+ // an invalid one (401 UNAUTHORIZED at every gate).
170
+ if (apiKey.expiresAt && apiKey.expiresAt.getTime() <= Date.now())
171
+ return null;
172
+ apiKey.lastUsedAt = new Date();
173
+ await em.flush();
174
+ return {
175
+ apiKeyId: apiKey.id,
176
+ scopes: apiKey.scopes,
177
+ organizationId: apiKey.organizationId ?? null,
178
+ salesChannelId: apiKey.salesChannelId ?? null,
179
+ customerAccountId: apiKey.customerAccountId ?? null,
180
+ };
181
+ }
182
+ }
183
+ function sha256Hex(input) {
184
+ return createHash('sha256').update(input, 'utf8').digest('hex');
185
+ }
186
+ //# sourceMappingURL=api-key-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api-key-service.js","sourceRoot":"","sources":["../../../src/backend/services/api-key-service.ts"],"names":[],"mappings":"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"}