@contractkit/plugin-typescript 0.21.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contractkit/plugin-typescript",
3
- "version": "0.21.0",
3
+ "version": "0.23.0",
4
4
  "description": "ContractKit built-in plugin: TypeScript codegen (SDK clients, Koa routers, Zod schemas, plain types)",
5
5
  "author": {
6
6
  "name": "Marooned Software",
@@ -26,11 +26,11 @@
26
26
  ".": "./dist/index.js"
27
27
  },
28
28
  "dependencies": {
29
- "@contractkit/core": "0.16.0"
29
+ "@contractkit/core": "0.17.0"
30
30
  },
31
31
  "devDependencies": {
32
- "@repo/config-eslint": "0.3.1",
33
- "@repo/config-typescript": "0.1.0"
32
+ "@repo/config-typescript": "0.1.0",
33
+ "@repo/config-eslint": "0.3.1"
34
34
  },
35
35
  "scripts": {
36
36
  "build": "tsup src/index.ts --format esm --sourcemap --dts && tsc --emitDeclarationOnly --declaration",
@@ -125,6 +125,7 @@ export interface OpCodegenOptions {
125
125
  includeInternal?: boolean;
126
126
  }
127
127
 
128
+ /** Generate a Koa router module for every operation in `root`, including the imports, type aliases, and handler list. */
128
129
  export function generateOp(root: OpRootNode, options: OpCodegenOptions = {}): string {
129
130
  // Collect all referenced types across all routes
130
131
  const types = collectTypes(root, options.modelsWithInput, options.modelsWithOutput);
@@ -240,8 +241,8 @@ function generateHandler(route: OpRouteNode, op: OpOperationNode, root: OpRootNo
240
241
  // Middleware list
241
242
  const middlewares: string[] = [];
242
243
  if (effectiveSecurity !== SECURITY_NONE) {
243
- const roles = effectiveSecurity && effectiveSecurity.roles?.length ? `roles: [${effectiveSecurity.roles.map(r => `'${r}'`).join(', ')}]` : '';
244
- middlewares.push(`requireSecurity({ ${roles} })`);
244
+ const args = effectiveSecurity && effectiveSecurity.requireMfa !== undefined ? `{ requireMfa: ${effectiveSecurity.requireMfa} }` : '';
245
+ middlewares.push(`requireSecurity(${args})`);
245
246
  }
246
247
  if (hasBody) {
247
248
  const parserTokens = Array.from(new Set(bodies.map(b => bodyParserToken(b.contentType))));
@@ -138,6 +138,7 @@ export function generateSdk(root: OpRootNode, options: SdkCodegenOptions = {}):
138
138
  lines.push(' public readonly status: number,');
139
139
  lines.push(' public readonly statusText: string,');
140
140
  lines.push(' public readonly body: unknown,');
141
+ lines.push(' public readonly headers: Headers,');
141
142
  lines.push(' ) {');
142
143
  lines.push(' super(`${status} ${statusText}`);');
143
144
  lines.push(" this.name = 'SdkError';");
@@ -168,7 +169,7 @@ export function generateSdk(root: OpRootNode, options: SdkCodegenOptions = {}):
168
169
  lines.push(' const text = await res.text();');
169
170
  lines.push(' let body: unknown;');
170
171
  lines.push(' try { body = JSON.parse(text); } catch { body = text; }');
171
- lines.push(' throw new SdkError(res.status, res.statusText, body);');
172
+ lines.push(' throw new SdkError(res.status, res.statusText, body, res.headers);');
172
173
  lines.push(' }');
173
174
  lines.push(' return res;');
174
175
  lines.push(' };');
@@ -885,6 +886,7 @@ export function generateSdkOptions(): string {
885
886
  ' public readonly status: number,',
886
887
  ' public readonly statusText: string,',
887
888
  ' public readonly body: unknown,',
889
+ ' public readonly headers: Headers,',
888
890
  ' ) {',
889
891
  ' super(`${status} ${statusText}`);',
890
892
  " this.name = 'SdkError';",
@@ -931,7 +933,7 @@ export function generateSdkOptions(): string {
931
933
  ' const text = await res.text();',
932
934
  ' let body: unknown;',
933
935
  ' try { body = JSON.parse(text); } catch { body = text; }',
934
- ' throw new SdkError(res.status, res.statusText, body);',
936
+ ' throw new SdkError(res.status, res.statusText, body, res.headers);',
935
937
  ' }',
936
938
  ' return res;',
937
939
  ' };',
@@ -807,9 +807,9 @@ describe('generateOp — route modifiers JSDoc', () => {
807
807
  expect(out).toContain('anonymous access, no security required');
808
808
  });
809
809
 
810
- it('emits no annotation for security with roles', () => {
810
+ it('emits no annotation for security with requireMfa', () => {
811
811
  const op = opOperation('get', {
812
- security: { roles: ['admin'], loc: { file: 'test.op', line: 1 } },
812
+ security: { requireMfa: true, loc: { file: 'test.op', line: 1 } },
813
813
  });
814
814
  const root = opRoot([opRoute('/users', [op])]);
815
815
  const out = generateOp(root);
@@ -867,7 +867,7 @@ describe('generateOp — route modifiers JSDoc', () => {
867
867
 
868
868
  it('does not import requireSignature when no signature is set', () => {
869
869
  const op = opOperation('get', {
870
- security: { roles: ['admin'], loc: { file: 'test.op', line: 1 } },
870
+ security: { requireMfa: true, loc: { file: 'test.op', line: 1 } },
871
871
  });
872
872
  const root = opRoot([opRoute('/users', [op])]);
873
873
  const out = generateOp(root);
@@ -876,7 +876,7 @@ describe('generateOp — route modifiers JSDoc', () => {
876
876
  });
877
877
  });
878
878
 
879
- // ─── Security (roles) middleware ────────────────────────────────
879
+ // ─── Security (requireMfa) middleware ───────────────────────────
880
880
 
881
881
  describe('security middleware', () => {
882
882
  it('injects requireSecurity() with no args for unannotated routes', () => {
@@ -884,27 +884,26 @@ describe('generateOp — route modifiers JSDoc', () => {
884
884
  const root = opRoot([opRoute('/users', [op])]);
885
885
  const out = generateOp(root);
886
886
  expect(out).toContain(`import { ServerKitRouter, bodyParserMiddleware, requireSecurity }`);
887
- expect(out).toContain(`requireSecurity({ })`);
887
+ expect(out).toContain(`requireSecurity()`);
888
888
  });
889
889
 
890
- it('injects requireSecurity with roles when roles are set', () => {
890
+ it('injects requireSecurity with requireMfa: true when set', () => {
891
891
  const op = opOperation('get', {
892
- security: { roles: ['admin'], loc: { file: 'test.op', line: 1 } },
892
+ security: { requireMfa: true, loc: { file: 'test.op', line: 1 } },
893
893
  });
894
894
  const root = opRoot([opRoute('/users', [op])]);
895
895
  const out = generateOp(root);
896
- expect(out).toContain(`requireSecurity({ roles: ['admin'] })`);
896
+ expect(out).toContain(`requireSecurity({ requireMfa: true })`);
897
897
  });
898
898
 
899
- it('passes multiple roles as an array', () => {
899
+ it('injects requireSecurity with requireMfa: false when set', () => {
900
900
  const op = opOperation('get', {
901
- security: { roles: ['admin', 'support'], loc: { file: 'test.op', line: 1 } },
901
+ security: { requireMfa: false, loc: { file: 'test.op', line: 1 } },
902
902
  });
903
- const root = opRoot([opRoute('/users', [op])]);
904
- const routeLine = generateOp(root)
903
+ const routeLine = generateOp(opRoot([opRoute('/users', [op])]))
905
904
  .split('\n')
906
905
  .find(l => l.includes('.get('));
907
- expect(routeLine).toContain(`requireSecurity({ roles: ['admin', 'support'] })`);
906
+ expect(routeLine).toContain(`requireSecurity({ requireMfa: false })`);
908
907
  });
909
908
 
910
909
  it('does not inject requireSecurity for public (security: none) routes', () => {
@@ -924,7 +923,7 @@ describe('generateOp — route modifiers JSDoc', () => {
924
923
 
925
924
  it('places requireSecurity before bodyParserMiddleware in the route line', () => {
926
925
  const op = opOperation('post', {
927
- security: { roles: ['admin'], loc: { file: 'test.op', line: 1 } },
926
+ security: { requireMfa: true, loc: { file: 'test.op', line: 1 } },
928
927
  request: opRequest('Payload'),
929
928
  });
930
929
  const root = opRoot([opRoute('/users', [op])]);
@@ -941,7 +940,7 @@ describe('generateOp — route modifiers JSDoc', () => {
941
940
  it('places requireSecurity before requireSignature when both are set', () => {
942
941
  const op = opOperation('post', {
943
942
  signature: 'MY_KEY',
944
- security: { roles: ['admin'], loc: { file: 'test.op', line: 1 } },
943
+ security: { requireMfa: true, loc: { file: 'test.op', line: 1 } },
945
944
  request: opRequest('Payload'),
946
945
  });
947
946
  const root = opRoot([opRoute('/webhooks', [op])]);
@@ -958,7 +957,7 @@ describe('generateOp — route modifiers JSDoc', () => {
958
957
  it('imports both requireSecurity and requireSignature when both are set', () => {
959
958
  const op = opOperation('post', {
960
959
  signature: 'MY_KEY',
961
- security: { roles: ['admin'], loc: { file: 'test.op', line: 1 } },
960
+ security: { requireMfa: true, loc: { file: 'test.op', line: 1 } },
962
961
  request: opRequest('Payload'),
963
962
  });
964
963
  const root = opRoot([opRoute('/webhooks', [op])]);
@@ -966,13 +965,13 @@ describe('generateOp — route modifiers JSDoc', () => {
966
965
  expect(out).toContain(`import { ServerKitRouter, bodyParserMiddleware, requireSecurity, requireSignature }`);
967
966
  });
968
967
 
969
- it('works with route-level roles security', () => {
968
+ it('works with route-level requireMfa security', () => {
970
969
  const op = opOperation('get');
971
970
  const route = opRoute('/users', [op]);
972
- route.security = { roles: ['admin'], loc: { file: 'test.op', line: 1 } };
971
+ route.security = { requireMfa: true, loc: { file: 'test.op', line: 1 } };
973
972
  const root = opRoot([route]);
974
973
  const out = generateOp(root);
975
- expect(out).toContain(`requireSecurity({ roles: ['admin'] })`);
974
+ expect(out).toContain(`requireSecurity({ requireMfa: true })`);
976
975
  });
977
976
  });
978
977
  });
@@ -648,6 +648,8 @@ describe('generateSdk', () => {
648
648
  expect(out).toContain('headers?:');
649
649
  expect(out).toContain('fetch?: SdkFetch');
650
650
  expect(out).toContain('export class SdkError extends Error');
651
+ expect(out).toContain('public readonly headers: Headers');
652
+ expect(out).toContain('throw new SdkError(res.status, res.statusText, body, res.headers)');
651
653
  });
652
654
  });
653
655
 
@@ -756,7 +758,8 @@ describe('generateSdkOptions', () => {
756
758
  expect(out).toContain('export class SdkError extends Error');
757
759
  expect(out).toContain('public readonly status: number');
758
760
  expect(out).toContain('public readonly body: unknown');
759
- expect(out).toContain('throw new SdkError(');
761
+ expect(out).toContain('public readonly headers: Headers');
762
+ expect(out).toContain('throw new SdkError(res.status, res.statusText, body, res.headers)');
760
763
  expect(out).toContain('export type SdkFetch');
761
764
  expect(out).toContain('export function createSdkFetch(options: SdkOptions): SdkFetch');
762
765
  expect(out).toContain('requestIdFactory ?? (() => crypto.randomUUID())');