@pikku/openapi-parser 0.12.13 → 0.12.15

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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @pikku/openapi-parser
2
2
 
3
+ ## 0.12.15
4
+
5
+ ### Patch Changes
6
+
7
+ - 854c342: Fix workspace addon integration: exclude nested pikku projects from inspection (prevents "More than one CoreUserSession/CoreConfig found" when a workspace addon is linked), widen the generated addon service `call()` data param to `unknown` so schema-less function inputs compile, and add `@pikku/inspector` + `@standard-schema/spec` to the generated addon devDependencies so its `.pikku` gen files typecheck.
8
+
9
+ ## 0.12.14
10
+
11
+ ### Patch Changes
12
+
13
+ - a10e88d: auth-config: new `extraHeaders` field — static headers baked into every generated request (the delegated login call and all proxied service calls), for upstreams that route on a header such as multi-tenant APIs resolving the tenant from `Origin`.
14
+
3
15
  ## 0.12.13
4
16
 
5
17
  ### Patch Changes
@@ -39,6 +39,7 @@ export declare const AuthConfigSchema: z.ZodObject<{
39
39
  raw: "raw";
40
40
  bearer: "bearer";
41
41
  }>>;
42
+ extraHeaders: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
42
43
  delegated: z.ZodOptional<z.ZodObject<{
43
44
  loginPath: z.ZodString;
44
45
  loginMethod: z.ZodDefault<z.ZodString>;
@@ -38,6 +38,13 @@ export const AuthConfigSchema = z.object({
38
38
  headerName: z.string().optional(),
39
39
  /** 'raw' sends the bare token; 'bearer' prefixes it. Default: bearer for Authorization, raw for custom headers. */
40
40
  headerFormat: z.enum(['raw', 'bearer']).optional(),
41
+ /**
42
+ * Static headers sent on EVERY request — the delegated login call and all
43
+ * proxied API calls. Needed for upstreams that route on a header, e.g. a
44
+ * multi-tenant API that resolves the tenant from an `Origin` header and
45
+ * rejects requests without it.
46
+ */
47
+ extraHeaders: z.record(z.string(), z.string()).optional(),
41
48
  /** Present when end-users sign in with their existing upstream credentials (delegated login). */
42
49
  delegated: DelegatedLoginSchema.optional(),
43
50
  });
package/dist/codegen.js CHANGED
@@ -839,7 +839,12 @@ function generateUpstreamAuthFile(vars, authConfig) {
839
839
  lines.push(` credentials: ${pascalName}UpstreamCredentials,`);
840
840
  lines.push(' baseUrl: string');
841
841
  lines.push(`): Promise<${pascalName}UpstreamIdentity | null> => {`);
842
- lines.push(" const headers: Record<string, string> = { 'Content-Type': 'application/json' }");
842
+ lines.push(' const headers: Record<string, string> = {');
843
+ lines.push(" 'Content-Type': 'application/json',");
844
+ for (const [header, value] of Object.entries(authConfig.extraHeaders ?? {})) {
845
+ lines.push(` ${JSON.stringify(header)}: ${JSON.stringify(value)},`);
846
+ }
847
+ lines.push(' }');
843
848
  lines.push(' const body: Record<string, string> = {}');
844
849
  if (delegated.credentials.includes('email')) {
845
850
  lines.push(' if (credentials.email) body.email = credentials.email');
@@ -1030,10 +1035,11 @@ function generateServiceFile(spec, opPairs, vars, flags) {
1030
1035
  lines.push(' async call<T>(');
1031
1036
  lines.push(" method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH',");
1032
1037
  lines.push(' path: string,');
1033
- lines.push(' data?: Record<string, unknown>');
1038
+ lines.push(' data?: unknown');
1034
1039
  lines.push(' ): Promise<T> {');
1040
+ lines.push(' const input = data as Record<string, unknown> | undefined');
1035
1041
  if (flags.camelCase) {
1036
- lines.push(' const rawData = data ? _toSnakeCase(data) : data');
1042
+ lines.push(' const rawData = input ? _toSnakeCase(input) : input');
1037
1043
  }
1038
1044
  lines.push(' const route = ROUTES[`${method} ${path}`]');
1039
1045
  lines.push(' let endpoint = path');
@@ -1041,10 +1047,13 @@ function generateServiceFile(spec, opPairs, vars, flags) {
1041
1047
  lines.push(' const query: Record<string, string> = {}');
1042
1048
  lines.push(' const headers: Record<string, string> = {');
1043
1049
  lines.push(" 'Content-Type': 'application/json',");
1050
+ for (const [header, value] of Object.entries(flags.authConfig?.extraHeaders ?? {})) {
1051
+ lines.push(` ${JSON.stringify(header)}: ${JSON.stringify(value)},`);
1052
+ }
1044
1053
  lines.push(' }');
1045
1054
  lines.push('');
1046
1055
  // When camelCase flag is on, use rawData (snake_case converted) for route matching
1047
- const dataVar = flags.camelCase ? 'rawData' : 'data';
1056
+ const dataVar = flags.camelCase ? 'rawData' : 'input';
1048
1057
  lines.push(` if (${dataVar} && route) {`);
1049
1058
  lines.push(' // Interpolate path params');
1050
1059
  lines.push(' for (const param of route.path) {');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/openapi-parser",
3
- "version": "0.12.13",
3
+ "version": "0.12.15",
4
4
  "description": "OpenAPI spec parser and code generator for Pikku",
5
5
  "author": "yasser.fadl@gmail.com",
6
6
  "license": "MIT",
@@ -41,6 +41,13 @@ export const AuthConfigSchema = z.object({
41
41
  headerName: z.string().optional(),
42
42
  /** 'raw' sends the bare token; 'bearer' prefixes it. Default: bearer for Authorization, raw for custom headers. */
43
43
  headerFormat: z.enum(['raw', 'bearer']).optional(),
44
+ /**
45
+ * Static headers sent on EVERY request — the delegated login call and all
46
+ * proxied API calls. Needed for upstreams that route on a header, e.g. a
47
+ * multi-tenant API that resolves the tenant from an `Origin` header and
48
+ * rejects requests without it.
49
+ */
50
+ extraHeaders: z.record(z.string(), z.string()).optional(),
44
51
  /** Present when end-users sign in with their existing upstream credentials (delegated login). */
45
52
  delegated: DelegatedLoginSchema.optional(),
46
53
  })
@@ -932,6 +932,29 @@ describe('auth config', () => {
932
932
  )
933
933
  })
934
934
 
935
+ test('extraHeaders are baked into the service and the upstream-auth login', () => {
936
+ const spec = makeSpec({ operations: [makeOp()] })
937
+ const files = generateAddonFromOpenAPI(spec, makeVars(), {
938
+ oauth: false,
939
+ secret: false,
940
+ credential: 'bearer',
941
+ authConfig: {
942
+ ...delegatedConfig,
943
+ extraHeaders: { Origin: 'https://tenant.example.com' },
944
+ },
945
+ })
946
+ const service = files['src/test-api-api.service.ts']
947
+ assert.ok(
948
+ service.includes('"Origin": "https://tenant.example.com",'),
949
+ 'service headers init should carry the static header'
950
+ )
951
+ const authFile = files['src/test-api-upstream-auth.ts']
952
+ assert.ok(
953
+ authFile.includes('"Origin": "https://tenant.example.com",'),
954
+ 'login fetch should carry the static header'
955
+ )
956
+ })
957
+
935
958
  test('no delegated config → no upstream-auth file', () => {
936
959
  const spec = makeSpec({ operations: [makeOp()] })
937
960
  const files = generateAddonFromOpenAPI(spec, makeVars(), {
package/src/codegen.ts CHANGED
@@ -1132,9 +1132,12 @@ function generateUpstreamAuthFile(
1132
1132
  lines.push(` credentials: ${pascalName}UpstreamCredentials,`)
1133
1133
  lines.push(' baseUrl: string')
1134
1134
  lines.push(`): Promise<${pascalName}UpstreamIdentity | null> => {`)
1135
- lines.push(
1136
- " const headers: Record<string, string> = { 'Content-Type': 'application/json' }"
1137
- )
1135
+ lines.push(' const headers: Record<string, string> = {')
1136
+ lines.push(" 'Content-Type': 'application/json',")
1137
+ for (const [header, value] of Object.entries(authConfig.extraHeaders ?? {})) {
1138
+ lines.push(` ${JSON.stringify(header)}: ${JSON.stringify(value)},`)
1139
+ }
1140
+ lines.push(' }')
1138
1141
  lines.push(' const body: Record<string, string> = {}')
1139
1142
  if (delegated.credentials.includes('email')) {
1140
1143
  lines.push(' if (credentials.email) body.email = credentials.email')
@@ -1394,10 +1397,13 @@ function generateServiceFile(
1394
1397
  lines.push(' async call<T>(')
1395
1398
  lines.push(" method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH',")
1396
1399
  lines.push(' path: string,')
1397
- lines.push(' data?: Record<string, unknown>')
1400
+ lines.push(' data?: unknown')
1398
1401
  lines.push(' ): Promise<T> {')
1402
+ lines.push(
1403
+ ' const input = data as Record<string, unknown> | undefined'
1404
+ )
1399
1405
  if (flags.camelCase) {
1400
- lines.push(' const rawData = data ? _toSnakeCase(data) : data')
1406
+ lines.push(' const rawData = input ? _toSnakeCase(input) : input')
1401
1407
  }
1402
1408
  lines.push(' const route = ROUTES[`${method} ${path}`]')
1403
1409
  lines.push(' let endpoint = path')
@@ -1405,10 +1411,15 @@ function generateServiceFile(
1405
1411
  lines.push(' const query: Record<string, string> = {}')
1406
1412
  lines.push(' const headers: Record<string, string> = {')
1407
1413
  lines.push(" 'Content-Type': 'application/json',")
1414
+ for (const [header, value] of Object.entries(
1415
+ flags.authConfig?.extraHeaders ?? {}
1416
+ )) {
1417
+ lines.push(` ${JSON.stringify(header)}: ${JSON.stringify(value)},`)
1418
+ }
1408
1419
  lines.push(' }')
1409
1420
  lines.push('')
1410
1421
  // When camelCase flag is on, use rawData (snake_case converted) for route matching
1411
- const dataVar = flags.camelCase ? 'rawData' : 'data'
1422
+ const dataVar = flags.camelCase ? 'rawData' : 'input'
1412
1423
  lines.push(` if (${dataVar} && route) {`)
1413
1424
  lines.push(' // Interpolate path params')
1414
1425
  lines.push(' for (const param of route.path) {')