@pylonsync/sdk 0.4.5 → 0.4.7

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/dist/index.d.ts CHANGED
@@ -403,16 +403,35 @@ export interface InputFieldDefinition {
403
403
  export interface QueryDefinition {
404
404
  name: string;
405
405
  input?: InputFieldDefinition[];
406
+ /**
407
+ * The function's declarative auth gate ("public" | "user" | …), as the
408
+ * router enforces it. Populated by `discoverFunctions()`; consumers
409
+ * (the OpenAPI generator) use it to mark which endpoints need
410
+ * credentials. Undefined means "not declared" — the runtime's default
411
+ * is "user".
412
+ */
413
+ auth?: string;
406
414
  }
407
415
  export declare function query(name: string, options?: {
408
416
  input?: InputFieldDefinition[];
417
+ auth?: string;
409
418
  }): QueryDefinition;
410
419
  export interface ActionDefinition {
411
420
  name: string;
412
421
  input?: InputFieldDefinition[];
422
+ /** See `QueryDefinition.auth`. */
423
+ auth?: string;
424
+ /**
425
+ * Which write kind this was before the manifest's read/write split
426
+ * collapsed mutations and actions into one bucket. Preserved so a
427
+ * generated spec can tell them apart.
428
+ */
429
+ fnType?: "mutation" | "action";
413
430
  }
414
431
  export declare function action(name: string, options?: {
415
432
  input?: InputFieldDefinition[];
433
+ auth?: string;
434
+ fnType?: "mutation" | "action";
416
435
  }): ActionDefinition;
417
436
  export interface PolicyDefinition {
418
437
  /** Optional — `buildManifest` auto-generates a name from the entity
@@ -568,10 +587,16 @@ export interface ManifestInputField {
568
587
  export interface ManifestQuery {
569
588
  name: string;
570
589
  input?: ManifestInputField[];
590
+ /** Declarative auth gate as the router enforces it. Absent → "user". */
591
+ auth?: string;
571
592
  }
572
593
  export interface ManifestAction {
573
594
  name: string;
574
595
  input?: ManifestInputField[];
596
+ /** Declarative auth gate as the router enforces it. Absent → "user". */
597
+ auth?: string;
598
+ /** "mutation" or "action" — the manifest bucket holds both. */
599
+ fn_type?: string;
575
600
  }
576
601
  export interface ManifestPolicy {
577
602
  name: string;
@@ -739,6 +764,44 @@ export declare function routesToManifest(routes: RouteDefinition[]): ManifestRou
739
764
  export declare function discoverAppRoutes(opts?: {
740
765
  appDir?: string;
741
766
  }): Promise<RouteDefinition[]>;
767
+ export interface DiscoveredFunctions {
768
+ queries: QueryDefinition[];
769
+ actions: ActionDefinition[];
770
+ }
771
+ /**
772
+ * Walk `functions/` and return every externally callable function, for
773
+ * `buildManifest({ queries, actions })`.
774
+ *
775
+ * The counterpart to `discoverAppRoutes()`. Without it, `buildManifest`
776
+ * takes `queries`/`actions` that nothing produces, so apps ship
777
+ * manifests describing zero callable endpoints while `/api/fn/<name>`
778
+ * serves dozens — `/api/manifest`, the generated OpenAPI spec, and
779
+ * `pylon codegen` all under-report in the same silent way.
780
+ *
781
+ * Discovery deliberately mirrors the runtime loader
782
+ * (`packages/functions/src/runtime.ts`) exactly: top-level `.ts`/`.js`
783
+ * files in `functions/`, no recursion, name = basename without the
784
+ * extension, and a default export whose `type` is a string and whose
785
+ * `handler` is a function. Anything the runtime would not register does
786
+ * not appear here, and vice versa — a manifest that disagrees with the
787
+ * router describes endpoints that don't exist.
788
+ *
789
+ * `internal: true` functions are excluded: the router refuses external
790
+ * calls to them with `FN_NOT_FOUND`, so listing them would document
791
+ * endpoints no client can reach.
792
+ *
793
+ * Queries go to `queries`; mutations and actions both go to `actions`,
794
+ * because the manifest's split is read vs write and has no third bucket.
795
+ * `fnType` on the entry preserves which one it was.
796
+ *
797
+ * Each file is dynamically imported, so function modules must be free of
798
+ * side effects at import time. That is already true of any module the
799
+ * runtime loads — this runs the same imports the server does — but it is
800
+ * a contract worth knowing about.
801
+ */
802
+ export declare function discoverFunctions(opts?: {
803
+ fnDir?: string;
804
+ }): Promise<DiscoveredFunctions>;
742
805
  export declare function queriesToManifest(queries: QueryDefinition[]): ManifestQuery[];
743
806
  export declare function actionsToManifest(actions: ActionDefinition[]): ManifestAction[];
744
807
  export declare function policiesToManifest(policies: PolicyDefinition[]): ManifestPolicy[];
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.4.5",
6
+ "version": "0.4.7",
7
7
  "type": "module",
8
8
  "main": "./src/index.ts",
9
9
  "types": "./dist/index.d.ts",
@@ -0,0 +1,207 @@
1
+ // Function discovery (discoverFunctions) — the functions/ walk that fills
2
+ // buildManifest's queries/actions. The correctness bar is agreement with the
3
+ // runtime loader in packages/functions/src/runtime.ts: a manifest that lists
4
+ // something the router won't serve documents an endpoint that doesn't exist,
5
+ // and one that omits a live function under-reports the whole API.
6
+
7
+ import { afterEach, describe, expect, test } from "bun:test";
8
+ import * as fs from "node:fs";
9
+ import * as os from "node:os";
10
+ import * as path from "node:path";
11
+ import { discoverFunctions } from "./index";
12
+
13
+ describe("discoverFunctions", () => {
14
+ const tmpdirs: string[] = [];
15
+ const prevCwd = process.cwd();
16
+ afterEach(() => {
17
+ process.chdir(prevCwd);
18
+ for (const d of tmpdirs.splice(0)) {
19
+ try {
20
+ fs.rmSync(d, { recursive: true, force: true });
21
+ } catch {
22
+ /* best effort */
23
+ }
24
+ }
25
+ });
26
+
27
+ /** Materialize a functions/ dir (filename → module source) and chdir in. */
28
+ function fns(files: Record<string, string>): void {
29
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "pylon-fns-"));
30
+ tmpdirs.push(root);
31
+ for (const [rel, src] of Object.entries(files)) {
32
+ const abs = path.join(root, "functions", rel);
33
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
34
+ fs.writeFileSync(abs, src);
35
+ }
36
+ process.chdir(root);
37
+ }
38
+
39
+ /**
40
+ * A function module shaped like `@pylonsync/functions` builds one. Written
41
+ * literally rather than imported so the SDK's test suite doesn't take a
42
+ * dependency on the functions package.
43
+ */
44
+ const fn = (
45
+ type: string,
46
+ args: string = "{}",
47
+ extra: string = "",
48
+ ): string =>
49
+ `export default { type: ${JSON.stringify(type)}, handler: () => null, args: ${args}${extra} };`;
50
+
51
+ test("splits queries from writes", async () => {
52
+ fns({
53
+ "listEvents.ts": fn("query"),
54
+ "saveEvent.ts": fn("mutation"),
55
+ "sendInvite.ts": fn("action"),
56
+ });
57
+ const { queries, actions } = await discoverFunctions();
58
+ expect(queries.map((q) => q.name)).toEqual(["listEvents"]);
59
+ // Mutations and actions share the write bucket; fnType keeps them apart.
60
+ expect(actions.map((a) => a.name).sort()).toEqual([
61
+ "saveEvent",
62
+ "sendInvite",
63
+ ]);
64
+ expect(actions.find((a) => a.name === "saveEvent")!.fnType).toBe("mutation");
65
+ expect(actions.find((a) => a.name === "sendInvite")!.fnType).toBe("action");
66
+ });
67
+
68
+ test("excludes internal functions", async () => {
69
+ // The router answers FN_NOT_FOUND for these, so listing one would
70
+ // document an endpoint no external client can reach.
71
+ fns({
72
+ "publicOne.ts": fn("query"),
73
+ "secretOne.ts": fn("query", "{}", ", internal: true"),
74
+ });
75
+ const { queries } = await discoverFunctions();
76
+ expect(queries.map((q) => q.name)).toEqual(["publicOne"]);
77
+ });
78
+
79
+ test("maps validators onto manifest field types", async () => {
80
+ fns({
81
+ "saveTrack.ts": fn(
82
+ "mutation",
83
+ `{
84
+ title: { type: "string" },
85
+ seats: { type: "int" },
86
+ price: { type: "float" },
87
+ live: { type: "bool" },
88
+ startsAt: { type: "datetime" },
89
+ eventId: { type: "id", table: "Event" }
90
+ }`,
91
+ ),
92
+ });
93
+ const { actions } = await discoverFunctions();
94
+ const byName = Object.fromEntries(
95
+ actions[0]!.input!.map((f) => [f.name, f.type]),
96
+ );
97
+ expect(byName).toEqual({
98
+ title: "string",
99
+ seats: "int",
100
+ price: "float",
101
+ live: "bool",
102
+ startsAt: "datetime",
103
+ eventId: "id(Event)",
104
+ });
105
+ });
106
+
107
+ test("reads optional off the validator and keeps the id table", async () => {
108
+ // v.optional(v.id("Event")) spreads the inner validator and adds
109
+ // optional:true, so `table` survives the wrapper.
110
+ fns({
111
+ "attach.ts": fn(
112
+ "mutation",
113
+ `{
114
+ eventId: { type: "id", table: "Event", optional: true },
115
+ note: { type: "string" }
116
+ }`,
117
+ ),
118
+ });
119
+ const { actions } = await discoverFunctions();
120
+ const input = actions[0]!.input!;
121
+ const eventId = input.find((f) => f.name === "eventId")!;
122
+ expect(eventId.optional).toBe(true);
123
+ expect(eventId.type).toBe("id(Event)");
124
+ expect(input.find((f) => f.name === "note")!.optional).toBe(false);
125
+ });
126
+
127
+ test("unmapped validators collapse to json", async () => {
128
+ // FieldType has no array or union variant. Documented lossiness —
129
+ // widening FieldType is what would preserve Validator.items.
130
+ fns({
131
+ "bulk.ts": fn(
132
+ "action",
133
+ `{
134
+ tags: { type: "array", items: { type: "string" } },
135
+ payload: { type: "any" },
136
+ blob: { type: "json" }
137
+ }`,
138
+ ),
139
+ });
140
+ const { actions } = await discoverFunctions();
141
+ for (const field of actions[0]!.input!) {
142
+ expect(field.type).toBe("json");
143
+ }
144
+ });
145
+
146
+ test("carries the declared auth gate", async () => {
147
+ fns({
148
+ "publicFeed.ts": fn("query", "{}", `, auth: "public"`),
149
+ "myProfile.ts": fn("query"),
150
+ });
151
+ const { queries } = await discoverFunctions();
152
+ expect(queries.find((q) => q.name === "publicFeed")!.auth).toBe("public");
153
+ // Undeclared stays undefined rather than being invented — the
154
+ // runtime's default is "user" and consumers apply it.
155
+ expect(queries.find((q) => q.name === "myProfile")!.auth).toBeUndefined();
156
+ });
157
+
158
+ test("skips modules whose default export is not a function definition", async () => {
159
+ fns({
160
+ "real.ts": fn("query"),
161
+ "config.ts": "export default { apiKey: 'x' };",
162
+ "noDefault.ts": "export const helper = () => null;",
163
+ // `type` present but no handler — the runtime's shape check
164
+ // rejects this too.
165
+ "halfBaked.ts": "export default { type: 'query' };",
166
+ });
167
+ const { queries, actions } = await discoverFunctions();
168
+ expect(queries.map((q) => q.name)).toEqual(["real"]);
169
+ expect(actions).toEqual([]);
170
+ });
171
+
172
+ test("a module that throws on import is skipped, not fatal", async () => {
173
+ fns({
174
+ "good.ts": fn("query"),
175
+ "explodes.ts": "throw new Error('boom');",
176
+ });
177
+ const { queries } = await discoverFunctions();
178
+ expect(queries.map((q) => q.name)).toEqual(["good"]);
179
+ });
180
+
181
+ test("no functions directory yields empty, not an error", async () => {
182
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "pylon-fns-"));
183
+ tmpdirs.push(root);
184
+ process.chdir(root);
185
+ expect(await discoverFunctions()).toEqual({ queries: [], actions: [] });
186
+ });
187
+
188
+ test("output is sorted so two machines agree", async () => {
189
+ fns({
190
+ "zebra.ts": fn("query"),
191
+ "alpha.ts": fn("query"),
192
+ "middle.ts": fn("query"),
193
+ });
194
+ const { queries } = await discoverFunctions();
195
+ expect(queries.map((q) => q.name)).toEqual(["alpha", "middle", "zebra"]);
196
+ });
197
+
198
+ test("ignores non-module files the runtime would also ignore", async () => {
199
+ fns({
200
+ "real.ts": fn("query"),
201
+ "notes.md": "# not a function",
202
+ "data.json": "{}",
203
+ });
204
+ const { queries } = await discoverFunctions();
205
+ expect(queries.map((q) => q.name)).toEqual(["real"]);
206
+ });
207
+ });
package/src/index.ts CHANGED
@@ -532,13 +532,21 @@ export interface InputFieldDefinition {
532
532
  export interface QueryDefinition {
533
533
  name: string;
534
534
  input?: InputFieldDefinition[];
535
+ /**
536
+ * The function's declarative auth gate ("public" | "user" | …), as the
537
+ * router enforces it. Populated by `discoverFunctions()`; consumers
538
+ * (the OpenAPI generator) use it to mark which endpoints need
539
+ * credentials. Undefined means "not declared" — the runtime's default
540
+ * is "user".
541
+ */
542
+ auth?: string;
535
543
  }
536
544
 
537
545
  export function query(
538
546
  name: string,
539
- options?: { input?: InputFieldDefinition[] }
547
+ options?: { input?: InputFieldDefinition[]; auth?: string }
540
548
  ): QueryDefinition {
541
- return { name, input: options?.input };
549
+ return { name, input: options?.input, auth: options?.auth };
542
550
  }
543
551
 
544
552
  // ---------------------------------------------------------------------------
@@ -548,13 +556,30 @@ export function query(
548
556
  export interface ActionDefinition {
549
557
  name: string;
550
558
  input?: InputFieldDefinition[];
559
+ /** See `QueryDefinition.auth`. */
560
+ auth?: string;
561
+ /**
562
+ * Which write kind this was before the manifest's read/write split
563
+ * collapsed mutations and actions into one bucket. Preserved so a
564
+ * generated spec can tell them apart.
565
+ */
566
+ fnType?: "mutation" | "action";
551
567
  }
552
568
 
553
569
  export function action(
554
570
  name: string,
555
- options?: { input?: InputFieldDefinition[] }
571
+ options?: {
572
+ input?: InputFieldDefinition[];
573
+ auth?: string;
574
+ fnType?: "mutation" | "action";
575
+ }
556
576
  ): ActionDefinition {
557
- return { name, input: options?.input };
577
+ return {
578
+ name,
579
+ input: options?.input,
580
+ auth: options?.auth,
581
+ fnType: options?.fnType,
582
+ };
558
583
  }
559
584
 
560
585
  // ---------------------------------------------------------------------------
@@ -745,11 +770,17 @@ export interface ManifestInputField {
745
770
  export interface ManifestQuery {
746
771
  name: string;
747
772
  input?: ManifestInputField[];
773
+ /** Declarative auth gate as the router enforces it. Absent → "user". */
774
+ auth?: string;
748
775
  }
749
776
 
750
777
  export interface ManifestAction {
751
778
  name: string;
752
779
  input?: ManifestInputField[];
780
+ /** Declarative auth gate as the router enforces it. Absent → "user". */
781
+ auth?: string;
782
+ /** "mutation" or "action" — the manifest bucket holds both. */
783
+ fn_type?: string;
753
784
  }
754
785
 
755
786
  export interface ManifestPolicy {
@@ -1322,6 +1353,190 @@ export async function discoverAppRoutes(opts?: {
1322
1353
  return [...navigable, ...boundaryRoutes];
1323
1354
  }
1324
1355
 
1356
+ // ---------------------------------------------------------------------------
1357
+ // Function discovery
1358
+ // ---------------------------------------------------------------------------
1359
+
1360
+ /**
1361
+ * A function definition as `@pylonsync/functions` builds it. Structural —
1362
+ * the SDK does not depend on that package (it would drag server-only
1363
+ * modules into client bundles), so this mirrors the shape the runtime
1364
+ * loader checks for.
1365
+ */
1366
+ interface RuntimeFnDefinition {
1367
+ type: "query" | "mutation" | "action";
1368
+ handler: unknown;
1369
+ args?: Record<string, RuntimeValidator>;
1370
+ internal?: boolean;
1371
+ auth?: string;
1372
+ }
1373
+
1374
+ /** A `v.*` validator: `{type, optional?, table?, items?}`. */
1375
+ interface RuntimeValidator {
1376
+ type: string;
1377
+ optional?: boolean;
1378
+ table?: string;
1379
+ }
1380
+
1381
+ /**
1382
+ * Map a validator onto the manifest's field type.
1383
+ *
1384
+ * `FieldType` has no array or union variant, so `v.array(...)`,
1385
+ * `v.union(...)`, and `v.any()` all land on `json`. The element type in
1386
+ * `Validator.items` is information this boundary throws away — widening
1387
+ * `FieldType` is the fix, and it would materially improve a generated
1388
+ * OpenAPI spec.
1389
+ */
1390
+ function validatorToFieldType(validator: RuntimeValidator): FieldType {
1391
+ switch (validator.type) {
1392
+ case "string":
1393
+ return "string";
1394
+ case "int":
1395
+ return "int";
1396
+ case "float":
1397
+ case "number":
1398
+ return "float";
1399
+ case "bool":
1400
+ case "boolean":
1401
+ return "bool";
1402
+ case "datetime":
1403
+ return "datetime";
1404
+ case "id":
1405
+ // `v.optional(v.id("Event"))` spreads the inner validator, so
1406
+ // `table` survives the optional wrapper.
1407
+ return validator.table ? (`id(${validator.table})` as FieldType) : "string";
1408
+ default:
1409
+ return "json";
1410
+ }
1411
+ }
1412
+
1413
+ function argsToInput(
1414
+ args: Record<string, RuntimeValidator> | undefined
1415
+ ): InputFieldDefinition[] {
1416
+ if (!args) return [];
1417
+ return Object.entries(args).map(([name, validator]) => ({
1418
+ name,
1419
+ type: validatorToFieldType(validator),
1420
+ // `v.optional(inner)` sets `optional: true` on the spread validator
1421
+ // itself, so this reads correctly without unwrapping.
1422
+ optional: validator.optional === true,
1423
+ }));
1424
+ }
1425
+
1426
+ export interface DiscoveredFunctions {
1427
+ queries: QueryDefinition[];
1428
+ actions: ActionDefinition[];
1429
+ }
1430
+
1431
+ /**
1432
+ * Walk `functions/` and return every externally callable function, for
1433
+ * `buildManifest({ queries, actions })`.
1434
+ *
1435
+ * The counterpart to `discoverAppRoutes()`. Without it, `buildManifest`
1436
+ * takes `queries`/`actions` that nothing produces, so apps ship
1437
+ * manifests describing zero callable endpoints while `/api/fn/<name>`
1438
+ * serves dozens — `/api/manifest`, the generated OpenAPI spec, and
1439
+ * `pylon codegen` all under-report in the same silent way.
1440
+ *
1441
+ * Discovery deliberately mirrors the runtime loader
1442
+ * (`packages/functions/src/runtime.ts`) exactly: top-level `.ts`/`.js`
1443
+ * files in `functions/`, no recursion, name = basename without the
1444
+ * extension, and a default export whose `type` is a string and whose
1445
+ * `handler` is a function. Anything the runtime would not register does
1446
+ * not appear here, and vice versa — a manifest that disagrees with the
1447
+ * router describes endpoints that don't exist.
1448
+ *
1449
+ * `internal: true` functions are excluded: the router refuses external
1450
+ * calls to them with `FN_NOT_FOUND`, so listing them would document
1451
+ * endpoints no client can reach.
1452
+ *
1453
+ * Queries go to `queries`; mutations and actions both go to `actions`,
1454
+ * because the manifest's split is read vs write and has no third bucket.
1455
+ * `fnType` on the entry preserves which one it was.
1456
+ *
1457
+ * Each file is dynamically imported, so function modules must be free of
1458
+ * side effects at import time. That is already true of any module the
1459
+ * runtime loads — this runs the same imports the server does — but it is
1460
+ * a contract worth knowing about.
1461
+ */
1462
+ export async function discoverFunctions(opts?: {
1463
+ fnDir?: string;
1464
+ }): Promise<DiscoveredFunctions> {
1465
+ // Same lazy node-builtin resolution as discoverAppRoutes: the SDK
1466
+ // carries no @types/node and must stay importable from client code.
1467
+ let fs: any;
1468
+ let path: any;
1469
+ let url: any;
1470
+ try {
1471
+ const nodeReq =
1472
+ (globalThis as any).require ??
1473
+ (await import("node:module")).createRequire(import.meta.url);
1474
+ fs = nodeReq("node:fs");
1475
+ path = nodeReq("node:path");
1476
+ url = nodeReq("node:url");
1477
+ } catch {
1478
+ return { queries: [], actions: [] };
1479
+ }
1480
+ if (!fs || !path || !url) return { queries: [], actions: [] };
1481
+
1482
+ const cwd = (globalThis as any).process?.cwd?.() ?? ".";
1483
+ const fnDir =
1484
+ opts?.fnDir && path.isAbsolute(opts.fnDir)
1485
+ ? opts.fnDir
1486
+ : path.join(cwd, opts?.fnDir ?? "functions");
1487
+ if (!fs.existsSync(fnDir) || !fs.statSync(fnDir).isDirectory()) {
1488
+ return { queries: [], actions: [] };
1489
+ }
1490
+
1491
+ const files: string[] = fs
1492
+ .readdirSync(fnDir)
1493
+ .filter((f: string) => f.endsWith(".ts") || f.endsWith(".js"))
1494
+ // Sorted so two machines produce byte-identical manifests.
1495
+ .sort();
1496
+
1497
+ const queries: QueryDefinition[] = [];
1498
+ const actions: ActionDefinition[] = [];
1499
+
1500
+ for (const file of files) {
1501
+ const name = file.replace(/\.(ts|js)$/, "");
1502
+ let definition: RuntimeFnDefinition | undefined;
1503
+ try {
1504
+ const mod = await import(
1505
+ url.pathToFileURL(path.join(fnDir, file)).href
1506
+ );
1507
+ definition = mod?.default as RuntimeFnDefinition | undefined;
1508
+ } catch {
1509
+ // A module that won't import can't be serving traffic either.
1510
+ // The runtime loader logs and skips; so do we, rather than
1511
+ // failing the whole manifest build over one bad file.
1512
+ continue;
1513
+ }
1514
+
1515
+ // The runtime's own registration check. A default export that isn't
1516
+ // a function definition (a config object, a React component in the
1517
+ // wrong directory) is silently skipped there and here.
1518
+ const shape = definition as unknown as Record<string, unknown> | undefined;
1519
+ if (
1520
+ !shape ||
1521
+ typeof shape.type !== "string" ||
1522
+ typeof shape.handler !== "function"
1523
+ ) {
1524
+ continue;
1525
+ }
1526
+ if (shape.internal === true) continue;
1527
+
1528
+ const input = argsToInput(definition!.args);
1529
+ const auth = typeof shape.auth === "string" ? shape.auth : undefined;
1530
+ if (definition!.type === "query") {
1531
+ queries.push({ name, input, auth });
1532
+ } else {
1533
+ actions.push({ name, input, auth, fnType: definition!.type });
1534
+ }
1535
+ }
1536
+
1537
+ return { queries, actions };
1538
+ }
1539
+
1325
1540
  export function queriesToManifest(queries: QueryDefinition[]): ManifestQuery[] {
1326
1541
  return queries.map((q) => {
1327
1542
  const result: ManifestQuery = { name: q.name };
@@ -1333,6 +1548,7 @@ export function queriesToManifest(queries: QueryDefinition[]): ManifestQuery[] {
1333
1548
  unique: false as const,
1334
1549
  }));
1335
1550
  }
1551
+ if (q.auth) result.auth = q.auth;
1336
1552
  return result;
1337
1553
  });
1338
1554
  }
@@ -1350,6 +1566,8 @@ export function actionsToManifest(
1350
1566
  unique: false as const,
1351
1567
  }));
1352
1568
  }
1569
+ if (a.auth) result.auth = a.auth;
1570
+ if (a.fnType) result.fn_type = a.fnType;
1353
1571
  return result;
1354
1572
  });
1355
1573
  }