@apso/cli 0.44.0 → 0.45.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/commands/generate.js +21 -1
  2. package/dist/commands/mcp/serve.js +30 -9
  3. package/dist/lib/generators/base.d.ts +1 -0
  4. package/dist/lib/generators/go.d.ts +2 -1
  5. package/dist/lib/generators/go.js +34 -4
  6. package/dist/lib/generators/python.d.ts +4 -2
  7. package/dist/lib/generators/python.js +28 -6
  8. package/dist/lib/generators/typescript.d.ts +2 -1
  9. package/dist/lib/generators/typescript.js +43 -6
  10. package/dist/lib/guards.d.ts +11 -1
  11. package/dist/lib/guards.js +20 -1
  12. package/dist/lib/templates/go/index-module.eta +6 -0
  13. package/dist/lib/templates/go/middleware/auth.eta +299 -59
  14. package/dist/lib/templates/guards/api-key-auth.guard.eta +146 -0
  15. package/dist/lib/templates/guards/auth.guard.eta +6 -0
  16. package/dist/lib/templates/guards/guards.module.eta +11 -19
  17. package/dist/lib/templates/guards/index.eta +2 -2
  18. package/dist/lib/templates/guards/jwt-auth.guard.eta +172 -0
  19. package/dist/lib/templates/guards/scope.guard.eta +20 -2
  20. package/dist/lib/templates/python/auth.eta +217 -0
  21. package/dist/lib/templates/python/index-module.eta +11 -0
  22. package/dist/lib/templates/python/models/model-col-enum.eta +1 -1
  23. package/dist/lib/templates/python/schemas/schema.eta +22 -1
  24. package/dist/lib/templates/rest/controller-rest.eta +16 -1
  25. package/dist/lib/templates/rest/index-module-rest.eta +6 -0
  26. package/dist/lib/types/auth.d.ts +41 -7
  27. package/dist/lib/types/auth.js +61 -2
  28. package/dist/lib/types/generator.d.ts +6 -0
  29. package/dist/lib/types/index.d.ts +1 -1
  30. package/dist/lib/types/index.js +3 -1
  31. package/dist/lib/utils/crud-deps.d.ts +13 -0
  32. package/dist/lib/utils/crud-deps.js +37 -1
  33. package/npm-shrinkwrap.json +2 -2
  34. package/oclif.manifest.json +1 -1
  35. package/package.json +1 -1
@@ -2,9 +2,11 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const tslib_1 = require("tslib");
4
4
  const core_1 = require("@oclif/core");
5
+ const fs = tslib_1.__importStar(require("fs"));
5
6
  const path = tslib_1.__importStar(require("path"));
6
7
  const inquirer_1 = tslib_1.__importDefault(require("inquirer"));
7
8
  const lib_1 = require("../lib");
9
+ const types_1 = require("../lib/types");
8
10
  const base_command_1 = tslib_1.__importDefault(require("../lib/base-command"));
9
11
  const interactive_1 = require("../lib/utils/interactive");
10
12
  const perf_hooks_1 = require("perf_hooks");
@@ -173,6 +175,7 @@ class Generate extends base_command_1.default {
173
175
  allEntities: entities,
174
176
  apiType: lowerCaseApiType,
175
177
  relationshipMap,
178
+ auth,
176
179
  }));
177
180
  }
178
181
  // eslint-disable-next-line no-await-in-loop
@@ -202,7 +205,7 @@ class Generate extends base_command_1.default {
202
205
  await writeGenerated(file.path, file.content);
203
206
  }
204
207
  // Generate index module
205
- const indexFiles = await generator.generateIndexModule(entities, lowerCaseApiType, { emitEvents, http });
208
+ const indexFiles = await generator.generateIndexModule(entities, lowerCaseApiType, { emitEvents, http, auth });
206
209
  for (const file of indexFiles) {
207
210
  // eslint-disable-next-line no-await-in-loop
208
211
  await writeGenerated(file.path, file.content);
@@ -265,6 +268,23 @@ class Generate extends base_command_1.default {
265
268
  console.log("[apso] Updated src/orm.config.ts so `apso migrate verify` and `apso test smoke` can run on a local PGlite database. Run npm install before building.");
266
269
  }
267
270
  }
271
+ // The generated auth code imports a JWT library; declare it.
272
+ if (auth && (0, types_1.isJwtAuth)(auth)) {
273
+ if (language === "typescript" &&
274
+ (0, crud_deps_1.addMissingDeps)(process.cwd(), crud_deps_1.JOSE_DEP).length > 0)
275
+ console.log("[apso] Added jose to package.json (the generated auth guard imports it). Run npm install before building.");
276
+ if (language === "python" &&
277
+ (0, crud_deps_1.addMissingPythonDep)(process.cwd(), crud_deps_1.PYJWT_DEP))
278
+ console.log(`[apso] Added ${crud_deps_1.PYJWT_DEP} to pyproject.toml (the generated auth module imports it). Reinstall the project's dependencies.`);
279
+ if (language === "go")
280
+ console.log("[apso] The generated auth middleware imports github.com/golang-jwt/jwt/v5 and github.com/MicahParks/keyfunc/v3. Run go mod tidy before building.");
281
+ }
282
+ if (language === "python") {
283
+ const main = path.join(process.cwd(), "app", "main.py");
284
+ if (fs.existsSync(main) &&
285
+ !fs.readFileSync(main, "utf8").includes("autogen_router"))
286
+ console.warn("[apso] app/main.py does not include autogen_router, so the generated routes are not served. Add: from app.autogen import autogen_router; app.include_router(autogen_router)");
287
+ }
268
288
  const totalBuildTime = perf_hooks_1.performance.now() - totalBuildStart;
269
289
  console.log(`[apso] Finished building all entities in ${totalBuildTime.toFixed(2)} ms`);
270
290
  // Best-effort: install the commit co-author hook so commits that include
@@ -382,6 +382,7 @@ class McpServe extends base_command_1.default {
382
382
  allEntities: entities,
383
383
  apiType: lowerCaseApiType,
384
384
  relationshipMap,
385
+ auth,
385
386
  }),
386
387
  generator.generateModule({
387
388
  entity,
@@ -408,7 +409,7 @@ class McpServe extends base_command_1.default {
408
409
  filesGenerated.push(file.path);
409
410
  }
410
411
  // Generate index module
411
- const indexFiles = await generator.generateIndexModule(entities, lowerCaseApiType);
412
+ const indexFiles = await generator.generateIndexModule(entities, lowerCaseApiType, { auth });
412
413
  for (const file of indexFiles) {
413
414
  const fullPath = path.join(autogenPath, file.path);
414
415
  // eslint-disable-next-line no-await-in-loop
@@ -450,6 +451,7 @@ class McpServe extends base_command_1.default {
450
451
  "auth0",
451
452
  "clerk",
452
453
  "cognito",
454
+ "jwt",
453
455
  "api-key",
454
456
  "custom-db-session",
455
457
  ])
@@ -518,11 +520,12 @@ class McpServe extends base_command_1.default {
518
520
  issuer: "https://YOUR_TENANT.auth0.com/",
519
521
  audience: "https://api.yourapp.com",
520
522
  },
523
+ // Auth0 custom claims must be namespaced URLs, added by a post-login Action.
521
524
  claims: {
522
525
  userId: "sub",
523
526
  email: "email",
524
- roles: "roles",
525
- organizationId: "org_id",
527
+ roles: "https://yourapp.com/roles",
528
+ workspaceId: "https://yourapp.com/org_id",
526
529
  },
527
530
  },
528
531
  entities: [],
@@ -531,15 +534,14 @@ class McpServe extends base_command_1.default {
531
534
  clerk: {
532
535
  auth: {
533
536
  provider: "clerk",
537
+ // Clerk session tokens carry no aud; the active organization is o.id / o.rol.
534
538
  jwt: {
535
539
  issuer: "https://YOUR_CLERK_FRONTEND_API",
536
- audience: "",
537
540
  },
538
541
  claims: {
539
542
  userId: "sub",
540
- email: "email",
541
- roles: "roles",
542
- organizationId: "org_id",
543
+ workspaceId: "o.id",
544
+ roles: "o.rol",
543
545
  },
544
546
  },
545
547
  entities: [],
@@ -556,7 +558,26 @@ class McpServe extends base_command_1.default {
556
558
  userId: "sub",
557
559
  email: "email",
558
560
  roles: "cognito:groups",
559
- organizationId: "custom:org_id",
561
+ workspaceId: "custom:org_id",
562
+ },
563
+ },
564
+ entities: [],
565
+ relationships: [],
566
+ },
567
+ jwt: {
568
+ // Any issuer that publishes a JWKS (Supabase Auth, Firebase, WorkOS, Okta, Better Auth's JWT plugin).
569
+ auth: {
570
+ provider: "jwt",
571
+ jwt: {
572
+ issuer: "https://YOUR_ISSUER",
573
+ audience: "YOUR_AUDIENCE",
574
+ jwksUri: "https://YOUR_ISSUER/.well-known/jwks.json",
575
+ },
576
+ claims: {
577
+ userId: "sub",
578
+ email: "email",
579
+ workspaceId: "org_id",
580
+ roles: "roles",
560
581
  },
561
582
  },
562
583
  entities: [],
@@ -601,7 +622,7 @@ class McpServe extends base_command_1.default {
601
622
  content: [
602
623
  {
603
624
  type: "text",
604
- text: `# Auth Setup: ${authProvider}\n\nMerge the following into your .apsorc file:\n\n\`\`\`json\n${JSON.stringify(config, null, 2)}\n\`\`\`\n\nAfter updating .apsorc:\n1. Run \`apso generate\` (or use the scaffold_api tool) to regenerate code\n2. Run \`npm install\` if new dependencies are needed\n3. Run \`apso dev\` to start the server and create auth tables\n\n${authProvider === "better-auth" ? "**Important:** BetterAuth stores passwords in the `account` table (not User). The `providerId` field must be set to \"credential\" for email/password login to work. Use `@apso/better-auth-adapter@latest` in your frontend." : "Configure the JWT issuer and audience values for your " + authProvider + " tenant."}`,
625
+ text: `# Auth Setup: ${authProvider}\n\nMerge the following into your .apsorc file:\n\n\`\`\`json\n${JSON.stringify(config, null, 2)}\n\`\`\`\n\nAfter updating .apsorc:\n1. Run \`apso generate\` (or use the scaffold_api tool) to regenerate code\n2. Run \`npm install\` if new dependencies are needed\n3. Run \`apso dev\` to start the server and create auth tables\n\n${authProvider === "better-auth" ? "**Important:** BetterAuth stores passwords in the `account` table (not User). The `providerId` field must be set to \"credential\" for email/password login to work. Use `@apso/better-auth-adapter@latest` in your frontend." : authProvider === "api-key" ? "Store each key's hex SHA-256 in ApiKey.keyHash, never the key itself; callers send the key in the x-api-key header." : "Replace the issuer, audience and claim names with your " + authProvider + " tenant's values. The service checks each token's signature against the issuer's JWKS. See https://docs.apso.ai/develop/auth/byoa."}`,
605
626
  },
606
627
  ],
607
628
  };
@@ -55,6 +55,7 @@ export declare abstract class BaseGenerator implements LanguageGenerator {
55
55
  abstract generateIndexModule(entities: Entity[], apiType: string, opts?: {
56
56
  emitEvents?: boolean;
57
57
  http?: boolean;
58
+ auth?: AuthConfig;
58
59
  }): Promise<GeneratedFile[]>;
59
60
  /**
60
61
  * Generate guard files (auth, scope, etc.)
@@ -35,6 +35,7 @@ export declare class GoGenerator extends BaseGenerator {
35
35
  generateIndexModule(entities: Entity[], _apiType: string, opts?: {
36
36
  emitEvents?: boolean;
37
37
  http?: boolean;
38
+ auth?: AuthConfig;
38
39
  }): Promise<GeneratedFile[]>;
39
40
  /**
40
41
  * Emits a schema-derived manifest of the entities that opted in to
@@ -54,6 +55,6 @@ export declare class GoGenerator extends BaseGenerator {
54
55
  generateDomainEvents(entities: Entity[], _apiType?: string, opts?: {
55
56
  emitEvents?: boolean;
56
57
  }): Promise<GeneratedFile[]>;
57
- generateGuards(_entities: Entity[], auth?: AuthConfig): Promise<GeneratedFile[]>;
58
+ generateGuards(entities: Entity[], auth?: AuthConfig): Promise<GeneratedFile[]>;
58
59
  generateQueryUtils(_entities: Entity[], _apiType: string): Promise<GeneratedFile[]>;
59
60
  }
@@ -5,6 +5,8 @@ const tslib_1 = require("tslib");
5
5
  const path = tslib_1.__importStar(require("path"));
6
6
  const Eta = tslib_1.__importStar(require("eta"));
7
7
  const base_1 = require("./base");
8
+ const types_1 = require("../types");
9
+ const guards_1 = require("../guards");
8
10
  const field_1 = require("../utils/field");
9
11
  const relationships_1 = require("../utils/relationships");
10
12
  const casing_1 = require("../utils/casing");
@@ -128,6 +130,8 @@ class GoGenerator extends base_1.BaseGenerator {
128
130
  goType: this.mapFieldToGoType(field),
129
131
  pascalName: (0, casing_1.pascalCase)(field.name),
130
132
  snakeName: (0, casing_1.snakeCase)(field.name),
133
+ // The enum type the enums package declares (EntityFieldEnum).
134
+ enumType: field.type === "enum" ? field.dataType : undefined,
131
135
  }));
132
136
  }
133
137
  async generateEntity(options) {
@@ -357,6 +361,7 @@ ${downStatements}
357
361
  });
358
362
  const content = await this.renderTemplate("./index-module", {
359
363
  entities: entitiesData,
364
+ hasAuth: Boolean(opts === null || opts === void 0 ? void 0 : opts.auth),
360
365
  });
361
366
  // Generate model registry for AutoMigrate support
362
367
  const modelRefs = entities
@@ -415,10 +420,35 @@ ${modelRefs}
415
420
  },
416
421
  ];
417
422
  }
418
- async generateGuards(_entities, auth) {
419
- const content = await this.renderTemplate("./middleware/auth", {
420
- auth,
421
- });
423
+ async generateGuards(entities, auth) {
424
+ if (!auth)
425
+ return [];
426
+ if ((0, guards_1.hasScopedEntities)(entities))
427
+ console.warn("[apso] scopeBy is enforced in TypeScript services only; this Go service authenticates requests but does not filter rows by tenant");
428
+ let data;
429
+ if ((0, types_1.isJwtAuth)(auth)) {
430
+ data = { jwt: (0, types_1.resolveJwtAuth)(auth) };
431
+ }
432
+ else if ((0, types_1.isApiKeyAuth)(auth)) {
433
+ const apiKey = (0, guards_1.apiKeyTemplateData)((0, types_1.resolveApiKeyAuth)(auth), entities);
434
+ const entity = entities.find((e) => e.name === apiKey.apiKeyEntity);
435
+ // gorm's default naming: snake_case of the Go field name.
436
+ const column = (name) => (0, casing_1.snakeCase)(name);
437
+ data = {
438
+ apiKey: {
439
+ ...apiKey,
440
+ goModel: (0, casing_1.pascalCase)(entity.name),
441
+ keyColumn: column(apiKey.keyField),
442
+ expiresColumn: column("expiresAt"),
443
+ workspaceColumn: column(apiKey.workspaceField),
444
+ roleColumn: apiKey.roleField ? column(apiKey.roleField) : undefined,
445
+ },
446
+ };
447
+ }
448
+ else {
449
+ throw new Error(`auth.provider "${auth.provider}" is generated for TypeScript services only. For Go, use "jwt" (Better Auth issues JWTs with its JWT plugin) or "api-key".`);
450
+ }
451
+ const content = await this.renderTemplate("./middleware/auth", data);
422
452
  return [
423
453
  {
424
454
  path: "middleware/auth.go",
@@ -16,7 +16,9 @@ export declare class PythonGenerator extends BaseGenerator {
16
16
  generateModule(_options: EntityGenerationOptions): Promise<GeneratedFile[]>;
17
17
  generateMigration(options: MigrationGenerationOptions): Promise<GeneratedFile[]>;
18
18
  generateEnums(entities: Entity[], _apiType: string): Promise<GeneratedFile[]>;
19
- generateIndexModule(entities: Entity[], _apiType: string): Promise<GeneratedFile[]>;
19
+ generateIndexModule(entities: Entity[], _apiType: string, opts?: {
20
+ auth?: AuthConfig;
21
+ }): Promise<GeneratedFile[]>;
20
22
  /**
21
23
  * Emits a schema-derived manifest of the entities that opted in to
22
24
  * domain-event emission (`.apsorc` `emitEvents`), mirroring the TypeScript
@@ -35,6 +37,6 @@ export declare class PythonGenerator extends BaseGenerator {
35
37
  generateDomainEvents(entities: Entity[], _apiType?: string, opts?: {
36
38
  emitEvents?: boolean;
37
39
  }): Promise<GeneratedFile[]>;
38
- generateGuards(_entities: Entity[], _auth?: AuthConfig): Promise<GeneratedFile[]>;
40
+ generateGuards(entities: Entity[], auth?: AuthConfig): Promise<GeneratedFile[]>;
39
41
  generateQueryUtils(_entities: Entity[], _apiType: string): Promise<GeneratedFile[]>;
40
42
  }
@@ -5,10 +5,12 @@ const tslib_1 = require("tslib");
5
5
  const path = tslib_1.__importStar(require("path"));
6
6
  const Eta = tslib_1.__importStar(require("eta"));
7
7
  const base_1 = require("./base");
8
+ const types_1 = require("../types");
8
9
  const field_1 = require("../utils/field");
9
10
  const relationships_1 = require("../utils/relationships");
10
11
  const casing_1 = require("../utils/casing");
11
12
  const events_1 = require("../events");
13
+ const guards_1 = require("../guards");
12
14
  const migration_file_1 = require("../ddl/migration-file");
13
15
  const pluralize_1 = tslib_1.__importDefault(require("pluralize"));
14
16
  /**
@@ -279,8 +281,11 @@ ${downStatements || " pass"}
279
281
  },
280
282
  ];
281
283
  }
282
- async generateIndexModule(entities, _apiType) {
283
- const content = await this.renderTemplate("./index-module", { entities });
284
+ async generateIndexModule(entities, _apiType, opts) {
285
+ const content = await this.renderTemplate("./index-module", {
286
+ entities,
287
+ hasAuth: Boolean(opts === null || opts === void 0 ? void 0 : opts.auth),
288
+ });
284
289
  return [
285
290
  {
286
291
  path: "__init__.py",
@@ -320,10 +325,27 @@ ${downStatements || " pass"}
320
325
  },
321
326
  ];
322
327
  }
323
- async generateGuards(_entities, _auth) {
324
- // Python auth/guards would be implemented differently (e.g., FastAPI dependencies)
325
- // This is a placeholder for future implementation
326
- return [];
328
+ async generateGuards(entities, auth) {
329
+ if (!auth)
330
+ return [];
331
+ if ((0, guards_1.hasScopedEntities)(entities))
332
+ console.warn("[apso] scopeBy is enforced in TypeScript services only; this Python service authenticates requests but does not filter rows by tenant");
333
+ let data;
334
+ if ((0, types_1.isJwtAuth)(auth)) {
335
+ data = { jwt: (0, types_1.resolveJwtAuth)(auth) };
336
+ }
337
+ else if ((0, types_1.isApiKeyAuth)(auth)) {
338
+ const apiKey = (0, guards_1.apiKeyTemplateData)((0, types_1.resolveApiKeyAuth)(auth), entities);
339
+ data = {
340
+ apiKey: { ...apiKey, pyModule: apiKey.apiKeyEntity.toLowerCase() },
341
+ };
342
+ }
343
+ else {
344
+ throw new Error(`auth.provider "${auth.provider}" is generated for TypeScript services only. For Python, use "jwt" (Better Auth issues JWTs with its JWT plugin) or "api-key".`);
345
+ }
346
+ return [
347
+ { path: "auth.py", content: await this.renderTemplate("./auth", data) },
348
+ ];
327
349
  }
328
350
  async generateQueryUtils(_entities, _apiType) {
329
351
  const queryContent = await this.renderTemplate("./utils/query", {});
@@ -16,8 +16,9 @@ export declare class TypeScriptGenerator extends BaseGenerator {
16
16
  generateModule(options: EntityGenerationOptions): Promise<GeneratedFile[]>;
17
17
  generateMigration(_options: MigrationGenerationOptions): Promise<GeneratedFile[]>;
18
18
  generateEnums(entities: Entity[], apiType: string): Promise<GeneratedFile[]>;
19
- generateIndexModule(entities: Entity[], apiType: string, _opts?: {
19
+ generateIndexModule(entities: Entity[], apiType: string, opts?: {
20
20
  emitEvents?: boolean;
21
+ auth?: AuthConfig;
21
22
  }): Promise<GeneratedFile[]>;
22
23
  generateGuards(entities: Entity[], auth?: AuthConfig): Promise<GeneratedFile[]>;
23
24
  /**
@@ -109,7 +109,8 @@ class TypeScriptGenerator extends base_1.BaseGenerator {
109
109
  ];
110
110
  }
111
111
  async generateController(options) {
112
- const { entity, relationships, relationshipMap } = options;
112
+ var _a, _b;
113
+ const { entity, relationships, relationshipMap, auth } = options;
113
114
  const { name: entityName } = entity;
114
115
  const relationshipsForTemplate = (0, relationships_1.getRelationshipForTemplate)(entityName, relationships);
115
116
  // Handle nested relationships for joins
@@ -135,6 +136,12 @@ class TypeScriptGenerator extends base_1.BaseGenerator {
135
136
  primaryKeyType,
136
137
  associations: [...new Set(relationshipsForTemplate)],
137
138
  nestedJoinsFile,
139
+ // Direct scopeBy fields @CrudAuth adds to every query once a caller is
140
+ // authenticated. Nested scopes (task.workspaceId) are checked per row by
141
+ // ScopeGuard only.
142
+ crudAuth: auth
143
+ ? ((_b = (_a = (0, guards_1.getScopedEntities)([entity])[0]) === null || _a === void 0 ? void 0 : _a.scopes) !== null && _b !== void 0 ? _b : []).filter((s) => s.direct)
144
+ : [],
138
145
  };
139
146
  const controllerContent = await this.renderTemplate("./rest/controller-rest", data);
140
147
  const specContent = await this.renderTemplate("./rest/controller-rest-spec", data);
@@ -307,8 +314,13 @@ class TypeScriptGenerator extends base_1.BaseGenerator {
307
314
  },
308
315
  ];
309
316
  }
310
- async generateIndexModule(entities, apiType, _opts) {
311
- const content = await this.renderTemplate(`./${apiType}/index-module-${apiType}`, { entities });
317
+ async generateIndexModule(entities, apiType, opts) {
318
+ // generateGuards emits GuardsModule whenever auth or scopeBy is configured;
319
+ // listing it here is what turns its global guards on.
320
+ const content = await this.renderTemplate(`./${apiType}/index-module-${apiType}`, {
321
+ entities,
322
+ hasGuards: Boolean(opts === null || opts === void 0 ? void 0 : opts.auth) || (0, guards_1.hasScopedEntities)(entities),
323
+ });
312
324
  return [
313
325
  {
314
326
  path: "index.ts",
@@ -319,20 +331,45 @@ class TypeScriptGenerator extends base_1.BaseGenerator {
319
331
  async generateGuards(entities, auth) {
320
332
  const hasScopes = (0, guards_1.hasScopedEntities)(entities);
321
333
  const normalizedAuth = this.normalizeAuthConfig(auth);
322
- if (!hasScopes && !normalizedAuth) {
334
+ const jwt = auth && (0, auth_1.isJwtAuth)(auth) ? (0, auth_1.resolveJwtAuth)(auth) : undefined;
335
+ const apiKey = auth && (0, auth_1.isApiKeyAuth)(auth)
336
+ ? (0, guards_1.apiKeyTemplateData)((0, auth_1.resolveApiKeyAuth)(auth), entities)
337
+ : undefined;
338
+ const hasAuth = Boolean(normalizedAuth || jwt || apiKey);
339
+ if (!hasScopes && !hasAuth) {
323
340
  return [];
324
341
  }
325
342
  const scopedEntities = (0, guards_1.getScopedEntities)(entities);
326
343
  const files = [];
344
+ const authEntities = normalizedAuth
345
+ ? [
346
+ normalizedAuth.sessionEntity,
347
+ normalizedAuth.userEntity,
348
+ ...(normalizedAuth.accountUserEntity
349
+ ? [normalizedAuth.accountUserEntity]
350
+ : []),
351
+ ]
352
+ : apiKey
353
+ ? [apiKey.apiKeyEntity]
354
+ : [];
327
355
  const templateData = {
328
356
  scopedEntities,
329
357
  authConfig: normalizedAuth,
358
+ hasAuth,
359
+ authProvider: auth === null || auth === void 0 ? void 0 : auth.provider,
360
+ authEntities,
361
+ jwt,
362
+ apiKey,
330
363
  generatedAt: new Date().toISOString(),
331
364
  generatedBy: "Apso CLI",
332
365
  };
333
366
  // Generate auth.guard.ts if auth is configured
334
- if (normalizedAuth) {
335
- const authGuardContent = await this.renderTemplate("./guards/auth.guard.eta", templateData);
367
+ if (hasAuth) {
368
+ const authGuardContent = await this.renderTemplate(jwt
369
+ ? "./guards/jwt-auth.guard.eta"
370
+ : apiKey
371
+ ? "./guards/api-key-auth.guard.eta"
372
+ : "./guards/auth.guard.eta", templateData);
336
373
  files.push({
337
374
  path: "guards/auth.guard.ts",
338
375
  content: authGuardContent,
@@ -1,4 +1,4 @@
1
- import { Entity } from "./types";
1
+ import { Entity, ResolvedApiKeyAuth } from "./types";
2
2
  /**
3
3
  * Represents a scope field configuration for template rendering
4
4
  */
@@ -28,3 +28,13 @@ export declare function getScopedEntities(entities: Entity[]): ScopedEntityConfi
28
28
  * Checks if any entities have scopeBy configured.
29
29
  */
30
30
  export declare function hasScopedEntities(entities: Entity[]): boolean;
31
+ /** API key settings plus what the key entity offers, as the guard templates use them. */
32
+ export interface ApiKeyTemplateData extends ResolvedApiKeyAuth {
33
+ hasStatus: boolean;
34
+ hasExpiresAt: boolean;
35
+ }
36
+ /**
37
+ * Check the API key entity exists and has the hash field, and note whether it
38
+ * has `status` and `expiresAt`, which the guard then enforces.
39
+ */
40
+ export declare function apiKeyTemplateData(apiKey: ResolvedApiKeyAuth, entities: Entity[]): ApiKeyTemplateData;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.hasScopedEntities = exports.getScopedEntities = void 0;
3
+ exports.apiKeyTemplateData = exports.hasScopedEntities = exports.getScopedEntities = void 0;
4
4
  const tslib_1 = require("tslib");
5
5
  const pluralize_1 = tslib_1.__importDefault(require("pluralize"));
6
6
  /**
@@ -62,3 +62,22 @@ function hasScopedEntities(entities) {
62
62
  return entities.some((entity) => entity.scopeBy);
63
63
  }
64
64
  exports.hasScopedEntities = hasScopedEntities;
65
+ /**
66
+ * Check the API key entity exists and has the hash field, and note whether it
67
+ * has `status` and `expiresAt`, which the guard then enforces.
68
+ */
69
+ function apiKeyTemplateData(apiKey, entities) {
70
+ var _a;
71
+ const entity = entities.find((e) => e.name === apiKey.apiKeyEntity);
72
+ if (!entity)
73
+ throw new Error(`auth.apiKeyEntity "${apiKey.apiKeyEntity}" is not an entity in .apsorc; add it with a "${apiKey.keyField}" text field`);
74
+ const fields = new Set(((_a = entity.fields) !== null && _a !== void 0 ? _a : []).map((f) => f.name));
75
+ if (!fields.has(apiKey.keyField))
76
+ throw new Error(`Entity ${apiKey.apiKeyEntity} needs a "${apiKey.keyField}" text field holding each key's hex SHA-256 (auth.keyField)`);
77
+ return {
78
+ ...apiKey,
79
+ hasStatus: fields.has("status"),
80
+ hasExpiresAt: fields.has("expiresAt"),
81
+ };
82
+ }
83
+ exports.apiKeyTemplateData = apiKeyTemplateData;
@@ -4,6 +4,7 @@ package routes
4
4
 
5
5
  import (
6
6
  <% if (httpEntities.length > 0) { %> "app/autogen/handlers"
7
+ <% } %><% if (it.hasAuth) { %> "app/autogen/middleware"
7
8
  <% } %> "app/autogen/services"
8
9
  "github.com/gin-gonic/gin"
9
10
  "gorm.io/gorm"
@@ -11,6 +12,11 @@ import (
11
12
 
12
13
  // RegisterAllRoutes registers all entity routes
13
14
  func RegisterAllRoutes(r *gin.RouterGroup, db *gorm.DB) {
15
+ <% if (it.hasAuth) { %>
16
+ // Every route in this group requires authentication (.apsorc auth).
17
+ r.Use(middleware.AuthMiddleware(db))
18
+
19
+ <% } %>
14
20
  <% httpEntities.forEach((entity) => { %>
15
21
  // <%= entity.name %> routes
16
22
  <%= entity.camelName %>Service := services.New<%= entity.name %>Service(db)