@tai42/api-client 0.1.0 → 0.1.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.
package/src/client.ts CHANGED
@@ -82,6 +82,23 @@ export interface TopicVerifierBody {
82
82
  readonly config?: Record<string, unknown>;
83
83
  }
84
84
 
85
+ /**
86
+ * Body for minting a trigger link (POST `/api/hooks/trigger-links`). `ttl_seconds`
87
+ * is REQUIRED and nullable — a positive int is a timed link, `null` is a permanent
88
+ * link (`0`/negative is a loud server 400); it is always sent EXPLICITLY (a dropped
89
+ * key 400s server-side). `name` is optional (the server generates one when omitted);
90
+ * `tool_kwargs` is the optional per-link params merged last into every fire.
91
+ */
92
+ export interface TriggerLinkCreateBody {
93
+ readonly topic: string;
94
+ readonly name?: string;
95
+ readonly execution_key: string;
96
+ // The link is a token door; this is its only auth knob (token → token+api_key).
97
+ readonly require_api_key: boolean;
98
+ readonly ttl_seconds: number | null;
99
+ readonly tool_kwargs?: Record<string, unknown> | null;
100
+ }
101
+
85
102
  /** Body for adding a URL to a scope (POST `/api/auth/scopes`). */
86
103
  export interface AddUrlToScopeBody {
87
104
  readonly scope_id: string;
@@ -89,6 +106,31 @@ export interface AddUrlToScopeBody {
89
106
  readonly pattern?: string;
90
107
  }
91
108
 
109
+ /**
110
+ * Body for creating a role (POST `/api/auth/roles`). `base_tier` is the security
111
+ * tier the role inherits (`editor`/`viewer` — `admin` is reserved); its jq is
112
+ * resolved server-side, never authored here. `grants` is the editable per-tag
113
+ * access-level map (feature-group tag → `none`/`read`/`write`); it defaults to
114
+ * empty (every group `none`, fail-closed) when omitted.
115
+ */
116
+ export interface RoleCreateBody {
117
+ readonly name: string;
118
+ readonly description?: string;
119
+ readonly base_tier: string;
120
+ readonly grants?: Record<string, s.GrantLevel>;
121
+ }
122
+
123
+ /**
124
+ * Body for editing a role (PUT `/api/auth/roles/{name}`). Both fields are
125
+ * omit-means-KEEP: an absent `grants` preserves the stored map (never a silent
126
+ * wipe), an absent `description` preserves the stored description. The base-tier jq
127
+ * is seed-fixed and not editable here.
128
+ */
129
+ export interface RoleUpdateBody {
130
+ readonly grants?: Record<string, s.GrantLevel>;
131
+ readonly description?: string;
132
+ }
133
+
92
134
  /**
93
135
  * Body for pinning a route public (POST `/api/auth/public-routes`). `pattern` is an
94
136
  * optional regex the AC verifier full-matches request paths against, mapping every
@@ -311,6 +353,13 @@ export function createApiClient(config: ApiConfig) {
311
353
  ) => apiRequest(config, path, schema, options);
312
354
 
313
355
  return {
356
+ // The configured API base — the exact `baseUrl` this client was constructed
357
+ // with (`''` for a same-origin deployment). Read-only: features that must
358
+ // compose an ABSOLUTE API URL (a QR-encoded public path the API origin must
359
+ // receive, not the SPA origin) read it instead of assuming same-origin. The
360
+ // transport itself still joins it per request; this only surfaces the value.
361
+ baseUrl: config.baseUrl ?? '',
362
+
314
363
  // -- tools ---------------------------------------------------------------
315
364
  listTools: (signal?: AbortSignal) => req('/api/tools', s.toolNames, { signal }),
316
365
  getToolSchema: (name: string, signal?: AbortSignal) =>
@@ -579,6 +628,32 @@ export function createApiClient(config: ApiConfig) {
579
628
  method: 'DELETE',
580
629
  }),
581
630
 
631
+ // -- hooks: trigger links -------------------------------------------------
632
+ // Mint a trigger link for a topic. `ttl_seconds` is sent EXPLICITLY
633
+ // (including `null` for a permanent link) — the server requires the key present
634
+ // and 400s a dropped one; `tool_kwargs` is omitted when undefined. The raw
635
+ // `token` rides the response ONCE (composed into the QR URL client-side).
636
+ createTriggerLink: (body: TriggerLinkCreateBody) =>
637
+ req('/api/hooks/trigger-links', s.triggerLinkCreated, {
638
+ method: 'POST',
639
+ body: {
640
+ topic: body.topic,
641
+ name: body.name,
642
+ execution_key: body.execution_key,
643
+ require_api_key: body.require_api_key,
644
+ ttl_seconds: body.ttl_seconds,
645
+ tool_kwargs: body.tool_kwargs,
646
+ },
647
+ }),
648
+ // Every live trigger-link record (hash prefix only — never a raw token).
649
+ listTriggerLinks: (signal?: AbortSignal) =>
650
+ req('/api/hooks/trigger-links', s.triggerLinkList, { signal }),
651
+ // Revoke a link by name (immediate + durable). 404 when the name is unknown.
652
+ deleteTriggerLink: (name: string) =>
653
+ req(`/api/hooks/trigger-links/${encodeSegment(name)}`, s.triggerLinkDeleted, {
654
+ method: 'DELETE',
655
+ }),
656
+
582
657
  // -- mcp config schema (manifest McpTab form) ----------------------------
583
658
  getMcpConfigSchema: (signal?: AbortSignal) =>
584
659
  req('/api/mcp-config/schema', s.jsonSchema, { signal }),
@@ -610,6 +685,35 @@ export function createApiClient(config: ApiConfig) {
610
685
  unpinPublicRoute: (url: string) =>
611
686
  req('/api/auth/public-routes', s.unpinPublicResult, { method: 'DELETE', body: { url } }),
612
687
 
688
+ // -- auth: roles (editable per-tag RBAC) ---------------------------------
689
+ // The seeded + operator-authored roles as full bodies (base-tier ceiling +
690
+ // editable per-tag grant map). ADMIN-ONLY — the route is `secret` (a listing
691
+ // exposes each role's raw base-tier jq); a non-admin projection never reaches it.
692
+ listRoles: (signal?: AbortSignal) => req('/api/auth/roles', s.roleList, { signal }),
693
+ // Create an operator-authored role. The server validates the grant map + base
694
+ // tier and 409s on a name collision; the reserved `admin` role cannot be created.
695
+ createRole: (body: RoleCreateBody) =>
696
+ req('/api/auth/roles', s.roleBody, { method: 'POST', body }),
697
+ // Edit a role's per-tag grant map + description (omit-means-keep). The reserved
698
+ // `admin` role and any `allow_all` role are block-downgrade guarded server-side.
699
+ updateRole: (name: string, body: RoleUpdateBody) =>
700
+ req(`/api/auth/roles/${encodeSegment(name)}`, s.roleBody, { method: 'PUT', body }),
701
+ // Delete a role. The reserved `admin` role is undeletable; a role still assigned
702
+ // to any principal is a loud 409.
703
+ deleteRole: (name: string) =>
704
+ req(`/api/auth/roles/${encodeSegment(name)}`, s.roleDeleted, { method: 'DELETE' }),
705
+ // A role's append-only version history + who/action/before→after audit trail.
706
+ // ADMIN-ONLY (`secret`).
707
+ listRoleVersions: (name: string, signal?: AbortSignal) =>
708
+ req(`/api/auth/roles/${encodeSegment(name)}/versions`, s.roleVersions, { signal }),
709
+ // Re-point a role's active version to a prior one (LIVE — holders follow on
710
+ // their next request); returns the re-pointed role body.
711
+ rollbackRole: (name: string, version: number) =>
712
+ req(`/api/auth/roles/${encodeSegment(name)}/rollback`, s.roleBody, {
713
+ method: 'POST',
714
+ body: { version },
715
+ }),
716
+
613
717
  // -- auth: api keys ------------------------------------------------------
614
718
  listTokensPayload: (signal?: AbortSignal) =>
615
719
  req('/api/auth/tokens-payload', s.tokensPayload, { signal }),
@@ -738,8 +842,8 @@ export function createApiClient(config: ApiConfig) {
738
842
  req(`/api/schedules/${encodeSegment(name)}`, s.jsonValue, { method: 'DELETE' }),
739
843
 
740
844
  // -- observability -------------------------------------------------------
741
- // NB: named `getObservabilityMetrics` (not `getMetrics`) — `getMetrics`
742
- // already serves the Prometheus `/metrics` text endpoint.
845
+ // The monitoring-backend dashboard rollups (`GET /api/observability/metrics`),
846
+ // a JSON aggregate distinct from any raw process-metrics text endpoint.
743
847
  getObservabilityMetrics: (params: MetricsQuery = {}, signal?: AbortSignal) =>
744
848
  req('/api/observability/metrics', s.dashboardMetrics, {
745
849
  query: { from: params.from, to: params.to, granularity: params.granularity },
@@ -801,9 +905,8 @@ export function createApiClient(config: ApiConfig) {
801
905
  getMarketplaceAdvisories: (signal?: AbortSignal) =>
802
906
  req('/api/marketplace/advisories', s.marketplaceAdvisories, { signal }),
803
907
 
804
- // -- system (plain-text ops endpoints) -----------------------------------
908
+ // -- system (plain-text ops endpoint) ------------------------------------
805
909
  getHealth: (signal?: AbortSignal) => apiText(config, '/health', { signal }),
806
- getMetrics: (signal?: AbortSignal) => apiText(config, '/metrics', { signal }),
807
910
 
808
911
  // The pluggable-kind status table (`GET /api/system/kinds`) — a JSON `{data}`
809
912
  // envelope, one row per kind with its active/default/off state.
package/src/http.ts CHANGED
@@ -129,9 +129,8 @@ export async function apiRequest<S extends z.ZodTypeAny>(
129
129
  }
130
130
 
131
131
  /**
132
- * Fetch a plain-text endpoint (the skeleton's `/health` → `"OK"` and `/metrics`
133
- * → Prometheus text). No `{ data }` envelope, no zod — the body is returned
134
- * verbatim. Non-2xx still throws loudly.
132
+ * Fetch a plain-text endpoint (the skeleton's `/health` → `"OK"`). No `{ data }`
133
+ * envelope, no zod — the body is returned verbatim. Non-2xx still throws loudly.
135
134
  */
136
135
  export async function apiText(
137
136
  config: ApiConfig,
package/src/index.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @tai42/api-client — the typed transport for the tai-skeleton HTTP API.
2
+ * @tai42/api-client — the typed transport for the tai42-skeleton HTTP API.
3
3
  *
4
4
  * One zod schema per consumed endpoint; every response is
5
5
  * validated and every failure throws. No caching of its own — server state
@@ -14,7 +14,10 @@ export type {
14
14
  ClaimLinkBody,
15
15
  ValidateConditionBody,
16
16
  TopicVerifierBody,
17
+ TriggerLinkCreateBody,
17
18
  AddUrlToScopeBody,
19
+ RoleCreateBody,
20
+ RoleUpdateBody,
18
21
  PinRoutePublicBody,
19
22
  CreatePresetBody,
20
23
  SavePresetVersionBody,
@@ -67,6 +70,13 @@ export type {
67
70
  ToolMediaResult,
68
71
  ToolTagEntry,
69
72
  AuthRoute,
73
+ RouteAction,
74
+ GrantLevel,
75
+ RoleGrants,
76
+ RoleBody,
77
+ RoleVersion,
78
+ RoleAuditEvent,
79
+ RoleVersions,
70
80
  PresetRecord,
71
81
  PresetDetail,
72
82
  PresetBody,
@@ -95,6 +105,8 @@ export type {
95
105
  Notification,
96
106
  Notifications,
97
107
  HookParams,
108
+ HookList,
109
+ TriggerAuth,
98
110
  SettingsSchema,
99
111
  TokensPayload,
100
112
  LoginFormField,
@@ -106,6 +118,10 @@ export type {
106
118
  PatternEntry,
107
119
  SubMcpEntry,
108
120
  ClaimLinkCreated,
121
+ TriggerLinkCreated,
122
+ TriggerLinkRecord,
123
+ TriggerLinkList,
124
+ TriggerLinkDeleted,
109
125
  LogoutResult,
110
126
  AuthCapabilities,
111
127
  BackupDocument,
package/src/schemas.ts CHANGED
@@ -679,14 +679,26 @@ export const notifications = z.object({
679
679
  export type Notifications = z.infer<typeof notifications>;
680
680
 
681
681
  // -- hooks -------------------------------------------------------------------
682
- // Shapes match tai_contract.hooks.HookParams: a hook fires a `tool` on a `topic`,
682
+ // Shapes match tai42_contract.hooks.HookParams: a hook fires a `tool` on a `topic`,
683
683
  // optionally gated by a `condition` and shaped by an `expr` (each either an inline
684
684
  // spec or a registered id, with its own kwargs).
685
685
 
686
+ /** A fire door — who may trigger a path, not what the fire may do. A string enum. */
687
+ export const triggerAuth = z.enum([
688
+ 'public',
689
+ 'verifier',
690
+ 'token',
691
+ 'token+api_key',
692
+ 'out-of-service',
693
+ ]);
694
+ export type TriggerAuth = z.infer<typeof triggerAuth>;
695
+
686
696
  export const hookParams = z.object({
687
697
  name: z.string(),
688
698
  topic: z.string(),
689
699
  tool: z.string(),
700
+ // The api-key `user_id` the fire runs AS.
701
+ execution_key: z.string().min(1),
690
702
  tool_kwargs: z.record(z.string(), z.unknown()).default({}),
691
703
  condition: z.string().nullable().default(null),
692
704
  condition_id: z.string().nullable().default(null),
@@ -711,7 +723,10 @@ export const hookList = z.object({
711
723
  z.object({ verifier: z.string(), config: z.record(z.string(), z.unknown()) }),
712
724
  )
713
725
  .default({}),
726
+ // Per-topic fire door (topic → door). Tolerant of an older response omitting it.
727
+ trigger_auth: z.record(z.string(), triggerAuth).default({}),
714
728
  });
729
+ export type HookList = z.infer<typeof hookList>;
715
730
 
716
731
  export const hookRegistered = z.object({
717
732
  registered: z.boolean(),
@@ -734,6 +749,58 @@ export const topicVerifierSet = z.object({ topic: z.string(), verifier: z.string
734
749
  /** `DELETE /api/hooks/topics/{topic}/verifier` — the unbound topic. */
735
750
  export const topicVerifierRemoved = z.object({ removed: z.boolean(), topic: z.string() });
736
751
 
752
+ // -- trigger links -----------------------------------------------------------
753
+ // A trigger link is a minted, token-bearing PUBLIC URL (`GET|POST /trigger/{token}`)
754
+ // that resolves to a hook TOPIC and fires it. Studio mints, lists, and revokes them
755
+ // on the hooks page; the QR encodes the composed absolute URL.
756
+
757
+ /**
758
+ * `POST /api/hooks/trigger-links` — a freshly minted trigger link. `trigger_path`
759
+ * is a relative path (`/trigger/<token>`, never an absolute URL — Studio composes
760
+ * the API origin); `token` is the raw token returned exactly ONCE, in this response
761
+ * (nothing else stores or lists it — a listed link's QR is unrecoverable by
762
+ * design). `expires_at` is an ISO-8601 instant, or `null` for a permanent link.
763
+ */
764
+ export const triggerLinkCreated = z.object({
765
+ name: z.string(),
766
+ trigger_path: z.string(),
767
+ token: z.string(),
768
+ topic: z.string(),
769
+ expires_at: z.string().nullable(),
770
+ });
771
+ export type TriggerLinkCreated = z.infer<typeof triggerLinkCreated>;
772
+
773
+ /**
774
+ * One listed trigger-link record (`GET /api/hooks/trigger-links` item). `tool_kwargs`
775
+ * is the per-link params merged last into every fire (`null` or `{}` = none);
776
+ * `created_by` is the ambient creator identity (`null` on an identity-less,
777
+ * gate-off deployment); `token_hash_prefix` is a short hash prefix for log
778
+ * correlation — never a raw token (none is stored).
779
+ */
780
+ export const triggerLinkRecord = z.object({
781
+ name: z.string(),
782
+ topic: z.string(),
783
+ execution_key: z.string().min(1),
784
+ trigger_auth: triggerAuth,
785
+ tool_kwargs: z.record(z.string(), z.unknown()).nullable(),
786
+ created_by: z.string().nullable(),
787
+ created_at: z.string(),
788
+ expires_at: z.string().nullable(),
789
+ token_hash_prefix: z.string(),
790
+ });
791
+ export type TriggerLinkRecord = z.infer<typeof triggerLinkRecord>;
792
+
793
+ /** `GET /api/hooks/trigger-links` — every live trigger-link record. */
794
+ export const triggerLinkList = z.object({
795
+ items: z.array(triggerLinkRecord),
796
+ total: z.number(),
797
+ });
798
+ export type TriggerLinkList = z.infer<typeof triggerLinkList>;
799
+
800
+ /** `DELETE /api/hooks/trigger-links/{name}` — the revoked link. */
801
+ export const triggerLinkDeleted = z.object({ removed: z.boolean(), name: z.string() });
802
+ export type TriggerLinkDeleted = z.infer<typeof triggerLinkDeleted>;
803
+
737
804
  // -- settings schema ---------------------------------------------------------
738
805
  // The settings form is auto-rendered from a declarative group/field description
739
806
  // the skeleton emits from its typed settings models.
@@ -771,20 +838,113 @@ export const addUrlToScopeResult = z.object({ scope_id: z.string(), url: z.strin
771
838
  export const removeUrlFromScopeResult = z.object({ url: z.string() });
772
839
  export const removeScopeResult = z.object({ scope_id: z.string(), deleted_keys: z.number() });
773
840
 
841
+ /**
842
+ * A route's authorization ACTION class, joined onto the catalog from the route
843
+ * registry. `read`/`write` routes are GRANTABLE — a role's per-tag level opens
844
+ * them; `fenced`/`secret` routes are ADMIN-ONLY and never grantable (no per-tag
845
+ * level reaches them). `null` marks a path the registry carries no metadata for
846
+ * (an unregistered/ungated path — no feature tags, no action).
847
+ */
848
+ export const routeAction = z.enum(['read', 'write', 'fenced', 'secret']);
849
+ export type RouteAction = z.infer<typeof routeAction>;
850
+
774
851
  /**
775
852
  * One row of the route catalog (`GET /api/auth/routes`) — a plain HTTP route with
776
- * its current mapping. `mapped` is the scope id, the public marker, or `null` when
777
- * the path is unmapped (the mapper's "unassigned" bucket source). `methods` is the
778
- * route's method set with `HEAD` removed.
853
+ * its current mapping, JOINED with its route-registry metadata. `mapped` is the
854
+ * scope id, the public marker, or `null` when the path is unmapped (the mapper's
855
+ * "unassigned" bucket source). `methods` is the route's method set with `HEAD`
856
+ * removed. `tags` are the route's feature-group labels, `summary` its one-line
857
+ * description, and `action` its authorization class — the join fields the Roles
858
+ * grant editor reads to derive grantable feature groups and mark the admin-only
859
+ * (fenced/secret) routes it must never offer a grant for. An unregistered path
860
+ * carries `tags: []`, `summary: ''`, `action: null`.
779
861
  */
780
862
  export const authRoute = z.object({
781
863
  path: z.string(),
782
864
  methods: z.array(z.string()),
783
865
  mapped: z.string().nullable(),
866
+ tags: z.array(z.string()),
867
+ summary: z.string(),
868
+ action: routeAction.nullable(),
784
869
  });
785
870
  export type AuthRoute = z.infer<typeof authRoute>;
786
871
  export const authRoutes = z.array(authRoute);
787
872
 
873
+ // -- auth: roles (editable per-tag RBAC) -------------------------------------
874
+
875
+ /** A role's per-tag ACCESS LEVEL: `none` (no access), `read`, or `write`. */
876
+ export const grantLevel = z.enum(['none', 'read', 'write']);
877
+ export type GrantLevel = z.infer<typeof grantLevel>;
878
+
879
+ /** A role's editable grant map: feature-group TAG name → the role's access level. */
880
+ export const roleGrants = z.record(z.string(), grantLevel);
881
+ export type RoleGrants = z.infer<typeof roleGrants>;
882
+
883
+ /**
884
+ * A role body (`GET /api/auth/roles`, and the write routes' echo). Layer 1 is the
885
+ * read-only base-tier ceiling — the seeded jq `condition` plus `base_tier`
886
+ * (`editor`/`viewer`, or `null` for the reserved `allow_all` admin). Layer 2 is the
887
+ * editable per-tag `grants` map. `allow_all` (admin) carries an empty `grants` map
888
+ * and reaches everything, un-lockable. `condition`/`condition_id`/`condition_kwargs`
889
+ * are the base-tier jq — READ-ONLY here (no raw-jq authoring surface).
890
+ */
891
+ export const roleBody = z.object({
892
+ name: z.string(),
893
+ description: z.string(),
894
+ scopes: z.array(z.string()),
895
+ condition: z.string().nullable(),
896
+ condition_id: z.string().nullable(),
897
+ condition_kwargs: jsonValue.nullable(),
898
+ base_tier: z.string().nullable(),
899
+ allow_all: z.boolean(),
900
+ grants: roleGrants,
901
+ });
902
+ export type RoleBody = z.infer<typeof roleBody>;
903
+ export const roleList = z.array(roleBody);
904
+
905
+ /** `DELETE /api/auth/roles/{name}` — the deleted role's name. */
906
+ export const roleDeleted = z.object({ name: z.string(), deleted: z.boolean() });
907
+
908
+ /**
909
+ * One immutable role-version row (`GET /api/auth/roles/{name}/versions` →
910
+ * `versions`). `body` is the role body at that version; `is_current` flags the
911
+ * active pointer. Mirrors the generic versioned-store row shape.
912
+ */
913
+ export const roleVersion = z.object({
914
+ version: z.number(),
915
+ body: roleBody,
916
+ tags: z.array(z.string()),
917
+ created_at: z.string(),
918
+ is_current: z.boolean(),
919
+ });
920
+ export type RoleVersion = z.infer<typeof roleVersion>;
921
+
922
+ /**
923
+ * One who/action/before→after audit row (`GET /api/auth/roles/{name}/versions` →
924
+ * `audit`). Its `body` carries the mutation kind, the actor, and the before/after
925
+ * role bodies (both `null` for a create's `before` / a delete's `after`).
926
+ */
927
+ export const roleAuditEvent = z.object({
928
+ version: z.number(),
929
+ body: z.object({
930
+ action: z.string(),
931
+ actor: z.string().nullable(),
932
+ before: jsonValue.nullable(),
933
+ after: jsonValue.nullable(),
934
+ }),
935
+ tags: z.array(z.string()),
936
+ created_at: z.string(),
937
+ is_current: z.boolean(),
938
+ });
939
+ export type RoleAuditEvent = z.infer<typeof roleAuditEvent>;
940
+
941
+ /** `GET /api/auth/roles/{name}/versions` — the append-only history + audit trail. */
942
+ export const roleVersions = z.object({
943
+ versions: z.array(roleVersion),
944
+ audit: z.array(roleAuditEvent),
945
+ });
946
+ export type RoleVersions = z.infer<typeof roleVersions>;
947
+
788
948
  /** `GET /api/auth/public-routes` — the sorted urls pinned to the public marker. */
789
949
  export const publicRoutes = z.array(z.string());
790
950
 
@@ -793,9 +953,12 @@ export const pinPublicResult = z.object({ url: z.string() });
793
953
  /** `DELETE /api/auth/public-routes` — the unpinned url (404 when not pinned). */
794
954
  export const unpinPublicResult = z.object({ url: z.string() });
795
955
 
956
+ /** The per-key payloads (`GET /api/auth/tokens-payload`). The stable per-mint
957
+ * `key_fingerprint` is nested under `policy_data`; the picker reads it there. */
796
958
  export const tokensPayload = z.array(
797
959
  z.object({
798
- user_id: z.string(),
960
+ // The value an `execution_key` binding names.
961
+ user_id: z.string().min(1),
799
962
  description: z.string(),
800
963
  scopes: z.array(z.string()),
801
964
  policy_data: jsonValue,
@@ -806,8 +969,11 @@ export const tokensPayload = z.array(
806
969
  );
807
970
  export type TokensPayload = z.infer<typeof tokensPayload>;
808
971
 
809
- /** The raw `sk-…` key string, returned once at creation time and never again. */
810
- export const createdApiKey = z.string();
972
+ /** Mint reply `{ api_key, key_fingerprint }` narrowed to the raw `sk-…` key,
973
+ * returned once at creation and never again. */
974
+ export const createdApiKey = z
975
+ .object({ api_key: z.string(), key_fingerprint: z.string() })
976
+ .transform((minted) => minted.api_key);
811
977
  export const editApiKeyResult = z.object({ user_id: z.string(), updated: z.boolean() });
812
978
  export const revokeApiKeyResult = z.object({ user_id: z.string(), revoked: z.boolean() });
813
979