@superblocksteam/shared 0.9585.1 → 0.9586.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.
Files changed (69) hide show
  1. package/dist/observability/index.d.ts +1 -0
  2. package/dist/observability/index.d.ts.map +1 -1
  3. package/dist/observability/index.js +2 -1
  4. package/dist/observability/index.js.map +1 -1
  5. package/dist/types/ai/quota-paywall.d.ts +1 -1
  6. package/dist/types/ai/quota-paywall.d.ts.map +1 -1
  7. package/dist/types/ai/quota-paywall.js +23 -0
  8. package/dist/types/ai/quota-paywall.js.map +1 -1
  9. package/dist/types/ai/quota-paywall.test.d.ts +2 -0
  10. package/dist/types/ai/quota-paywall.test.d.ts.map +1 -0
  11. package/dist/types/ai/quota-paywall.test.js +69 -0
  12. package/dist/types/ai/quota-paywall.test.js.map +1 -0
  13. package/dist/types/application/index.d.ts +1 -1
  14. package/dist/types/application/index.d.ts.map +1 -1
  15. package/dist/types/application/index.js.map +1 -1
  16. package/dist/types/credentials/index.d.ts +253 -0
  17. package/dist/types/credentials/index.d.ts.map +1 -0
  18. package/dist/types/credentials/index.js +134 -0
  19. package/dist/types/credentials/index.js.map +1 -0
  20. package/dist/types/index.d.ts +1 -0
  21. package/dist/types/index.d.ts.map +1 -1
  22. package/dist/types/index.js +1 -0
  23. package/dist/types/index.js.map +1 -1
  24. package/dist/types/organization/index.d.ts +16 -0
  25. package/dist/types/organization/index.d.ts.map +1 -1
  26. package/dist/types/organization/index.js.map +1 -1
  27. package/dist/types/rbac/index.d.ts +4 -0
  28. package/dist/types/rbac/index.d.ts.map +1 -1
  29. package/dist/types/rbac/index.js +4 -0
  30. package/dist/types/rbac/index.js.map +1 -1
  31. package/dist-esm/observability/index.d.ts +1 -0
  32. package/dist-esm/observability/index.d.ts.map +1 -1
  33. package/dist-esm/observability/index.js +1 -0
  34. package/dist-esm/observability/index.js.map +1 -1
  35. package/dist-esm/types/ai/quota-paywall.d.ts +1 -1
  36. package/dist-esm/types/ai/quota-paywall.d.ts.map +1 -1
  37. package/dist-esm/types/ai/quota-paywall.js +23 -0
  38. package/dist-esm/types/ai/quota-paywall.js.map +1 -1
  39. package/dist-esm/types/ai/quota-paywall.test.d.ts +2 -0
  40. package/dist-esm/types/ai/quota-paywall.test.d.ts.map +1 -0
  41. package/dist-esm/types/ai/quota-paywall.test.js +67 -0
  42. package/dist-esm/types/ai/quota-paywall.test.js.map +1 -0
  43. package/dist-esm/types/application/index.d.ts +1 -1
  44. package/dist-esm/types/application/index.d.ts.map +1 -1
  45. package/dist-esm/types/application/index.js.map +1 -1
  46. package/dist-esm/types/credentials/index.d.ts +253 -0
  47. package/dist-esm/types/credentials/index.d.ts.map +1 -0
  48. package/dist-esm/types/credentials/index.js +128 -0
  49. package/dist-esm/types/credentials/index.js.map +1 -0
  50. package/dist-esm/types/index.d.ts +1 -0
  51. package/dist-esm/types/index.d.ts.map +1 -1
  52. package/dist-esm/types/index.js +1 -0
  53. package/dist-esm/types/index.js.map +1 -1
  54. package/dist-esm/types/organization/index.d.ts +16 -0
  55. package/dist-esm/types/organization/index.d.ts.map +1 -1
  56. package/dist-esm/types/organization/index.js.map +1 -1
  57. package/dist-esm/types/rbac/index.d.ts +4 -0
  58. package/dist-esm/types/rbac/index.d.ts.map +1 -1
  59. package/dist-esm/types/rbac/index.js +4 -0
  60. package/dist-esm/types/rbac/index.js.map +1 -1
  61. package/package.json +1 -1
  62. package/src/observability/index.ts +1 -0
  63. package/src/types/ai/quota-paywall.test.ts +81 -0
  64. package/src/types/ai/quota-paywall.ts +33 -1
  65. package/src/types/application/index.ts +1 -2
  66. package/src/types/credentials/index.ts +268 -0
  67. package/src/types/index.ts +1 -0
  68. package/src/types/organization/index.ts +17 -0
  69. package/src/types/rbac/index.ts +4 -0
@@ -0,0 +1,81 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { AI_QUOTA_PAYWALL_REASON_CONFIG, getAiQuotaPaywallReasonFromMessage } from './quota-paywall';
4
+
5
+ describe('getAiQuotaPaywallReasonFromMessage', () => {
6
+ it('returns no_seat_assigned for various seat-missing phrasings', () => {
7
+ expect(getAiQuotaPaywallReasonFromMessage('no_seat_assigned')).toBe('no_seat_assigned');
8
+ expect(getAiQuotaPaywallReasonFromMessage("You don't have an AI Builder license assigned. Ask your admin to assign a license.")).toBe(
9
+ 'no_seat_assigned'
10
+ );
11
+ expect(getAiQuotaPaywallReasonFromMessage("user doesn't have an AI Builder license assigned")).toBe('no_seat_assigned');
12
+ expect(getAiQuotaPaywallReasonFromMessage('user does not have an AI Builder seat assigned')).toBe('no_seat_assigned');
13
+ expect(getAiQuotaPaywallReasonFromMessage('No builder seat assigned to user')).toBe('no_seat_assigned');
14
+ });
15
+
16
+ it('returns payment_past_due for the dedicated phrasings, including the dashboard-chat router copy', () => {
17
+ expect(getAiQuotaPaywallReasonFromMessage('payment_past_due')).toBe('payment_past_due');
18
+ expect(getAiQuotaPaywallReasonFromMessage('Your subscription payment failed. Update your payment method to continue.')).toBe(
19
+ 'payment_past_due'
20
+ );
21
+ expect(getAiQuotaPaywallReasonFromMessage('Your payment is past due')).toBe('payment_past_due');
22
+ });
23
+
24
+ it('returns trial_expired for the dashboard-chat router copy and other variants', () => {
25
+ expect(getAiQuotaPaywallReasonFromMessage('trial_expired')).toBe('trial_expired');
26
+ expect(getAiQuotaPaywallReasonFromMessage('Your AI trial period has ended')).toBe('trial_expired');
27
+ expect(getAiQuotaPaywallReasonFromMessage('Your free trial has ended')).toBe('trial_expired');
28
+ });
29
+
30
+ it('returns token_limit_exceeded for the generic fallback message', () => {
31
+ expect(getAiQuotaPaywallReasonFromMessage('token_limit_exceeded')).toBe('token_limit_exceeded');
32
+ expect(getAiQuotaPaywallReasonFromMessage('AI usage limit reached')).toBe('token_limit_exceeded');
33
+ });
34
+
35
+ it('returns deploy_quota_exceeded for the deploy variant', () => {
36
+ expect(getAiQuotaPaywallReasonFromMessage('deploy_quota_exceeded')).toBe('deploy_quota_exceeded');
37
+ });
38
+
39
+ it('returns credit_limit_exceeded when the org credit copy fires', () => {
40
+ expect(getAiQuotaPaywallReasonFromMessage('credit_limit_exceeded')).toBe('credit_limit_exceeded');
41
+ expect(getAiQuotaPaywallReasonFromMessage('Your organization has reached its AI usage limit')).toBe('credit_limit_exceeded');
42
+ });
43
+
44
+ it('is case insensitive', () => {
45
+ expect(getAiQuotaPaywallReasonFromMessage('PAYMENT_PAST_DUE')).toBe('payment_past_due');
46
+ expect(getAiQuotaPaywallReasonFromMessage('YOUR FREE TRIAL HAS ENDED')).toBe('trial_expired');
47
+ });
48
+
49
+ it('returns undefined when the message does not match any known phrasing', () => {
50
+ expect(getAiQuotaPaywallReasonFromMessage('Connection refused')).toBeUndefined();
51
+ expect(getAiQuotaPaywallReasonFromMessage('')).toBeUndefined();
52
+ });
53
+ });
54
+
55
+ describe('AI_QUOTA_PAYWALL_REASON_CONFIG', () => {
56
+ it('has a config entry for every AiQuotaPaywallReason', () => {
57
+ // If a new reason is added without a config entry, the modal will fall back to
58
+ // undefined `title`/`subtitle` strings — better to fail at compile time AND at
59
+ // runtime here.
60
+ const expectedReasons = [
61
+ 'credit_limit_exceeded',
62
+ 'deploy_quota_exceeded',
63
+ 'no_seat_assigned',
64
+ 'payment_past_due',
65
+ 'token_limit_exceeded',
66
+ 'trial_expired'
67
+ ] as const;
68
+ for (const r of expectedReasons) {
69
+ expect(AI_QUOTA_PAYWALL_REASON_CONFIG[r]).toBeDefined();
70
+ expect(AI_QUOTA_PAYWALL_REASON_CONFIG[r].title.length).toBeGreaterThan(0);
71
+ expect(AI_QUOTA_PAYWALL_REASON_CONFIG[r].subtitle.length).toBeGreaterThan(0);
72
+ }
73
+ });
74
+
75
+ it('payment_past_due copy mentions payment / subscription', () => {
76
+ const cfg = AI_QUOTA_PAYWALL_REASON_CONFIG.payment_past_due;
77
+ const text = `${cfg.title} ${cfg.subtitle}`.toLowerCase();
78
+ expect(text).toContain('payment');
79
+ expect(text).toMatch(/subscription|paid features/);
80
+ });
81
+ });
@@ -1,9 +1,12 @@
1
1
  export type AiQuotaPaywallReason =
2
2
  | 'credit_limit_exceeded'
3
3
  | 'deploy_quota_exceeded'
4
+ | 'dollar_commit_exhausted'
4
5
  | 'no_seat_assigned'
6
+ | 'payment_past_due'
5
7
  | 'token_limit_exceeded'
6
- | 'trial_expired';
8
+ | 'trial_expired'
9
+ | 'user_credit_limit_exceeded';
7
10
 
8
11
  export const getAiQuotaPaywallReasonFromMessage = (message: string): AiQuotaPaywallReason | undefined => {
9
12
  const normalizedMessage = message.toLowerCase();
@@ -21,6 +24,14 @@ export const getAiQuotaPaywallReasonFromMessage = (message: string): AiQuotaPayw
21
24
  return 'no_seat_assigned';
22
25
  }
23
26
 
27
+ if (
28
+ normalizedMessage.includes('payment_past_due') ||
29
+ normalizedMessage.includes('payment is past due') ||
30
+ normalizedMessage.includes('subscription payment failed')
31
+ ) {
32
+ return 'payment_past_due';
33
+ }
34
+
24
35
  if (
25
36
  normalizedMessage.includes('trial_expired') ||
26
37
  normalizedMessage.includes('ai trial period has ended') ||
@@ -37,6 +48,14 @@ export const getAiQuotaPaywallReasonFromMessage = (message: string): AiQuotaPayw
37
48
  return 'deploy_quota_exceeded';
38
49
  }
39
50
 
51
+ if (normalizedMessage.includes('dollar_commit_exhausted')) {
52
+ return 'dollar_commit_exhausted';
53
+ }
54
+
55
+ if (normalizedMessage.includes('user_credit_limit_exceeded') || normalizedMessage.includes("you've reached your assigned usage limit")) {
56
+ return 'user_credit_limit_exceeded';
57
+ }
58
+
40
59
  if (normalizedMessage.includes('credit_limit_exceeded') || normalizedMessage.includes('organization has reached its ai usage limit')) {
41
60
  return 'credit_limit_exceeded';
42
61
  }
@@ -74,6 +93,19 @@ export const AI_QUOTA_PAYWALL_REASON_CONFIG: Record<AiQuotaPaywallReason, { titl
74
93
  deploy_quota_exceeded: {
75
94
  title: 'Deploy limit reached',
76
95
  subtitle: 'Your organization has reached its deployed app limit. Add more deployed apps to continue.'
96
+ },
97
+ dollar_commit_exhausted: {
98
+ title: 'Dollar commit pool exhausted',
99
+ subtitle: 'Your organization has used its entire dollar commit pool. Contact your account team to adjust your contract.'
100
+ },
101
+ payment_past_due: {
102
+ title: "Your last payment didn't go through",
103
+ subtitle:
104
+ 'Update your payment method to restore access to Clark AI and other paid features. Your subscription is paused until the outstanding invoice is paid.'
105
+ },
106
+ user_credit_limit_exceeded: {
107
+ title: "You've reached your assigned usage limit",
108
+ subtitle: 'Contact an admin to request an increase.'
77
109
  }
78
110
  };
79
111
 
@@ -623,8 +623,7 @@ export type ApplicationHashChangeSource =
623
623
  | 'migrate:backup'
624
624
  // Explicit completion checkpoint after v2->v3 migration succeeds and local
625
625
  // scratch artifacts are removed.
626
- | 'migrate:complete'
627
- | 'shutdownSync';
626
+ | 'migrate:complete';
628
627
 
629
628
  /**
630
629
  * Returns true if the application's template uses the SDK API structure.
@@ -0,0 +1,268 @@
1
+ /**
2
+ * Per-organization credential payloads, shared between server and UI.
3
+ *
4
+ * A credential row is canonically identified by the
5
+ * `(role, backend, auth_method)` tuple; the schema stores the tuple,
6
+ * not a dotted-string discriminator. The dotted form
7
+ * `<role>.<backend>.<auth-method>` clients see in `type` is a derived
8
+ * label projected from the tuple at DTO-build time. Always 3 segments
9
+ * — even when a backend has a single canonical auth method, it's
10
+ * recorded explicitly so the wire format and the schema stay in
11
+ * lockstep without a per-backend special case.
12
+ *
13
+ * Each row carries `{ secrets, metadata }`. The database enforces the
14
+ * split at the column level: `secrets` is encrypted, `metadata` is
15
+ * plaintext jsonb. The union below forces every new field to pick a
16
+ * side at authoring time — a future column cannot accidentally stuff
17
+ * a secret into `metadata` without the TS compiler flagging it,
18
+ * because the keys in each variant's `metadata` are fixed.
19
+ *
20
+ * The registry (`packages/server/src/db/repository/credential/registry.ts`)
21
+ * owns the matching zod validators, the runtime-plaintext-location
22
+ * pin, and the cardinality (`'singleton' | 'multi' | 'per-user'`).
23
+ * This file only expresses the shape both sides of the wire agree on.
24
+ *
25
+ * - `provisioning.*` — admin credentials a worker uses to create
26
+ * per-app logical databases in a customer's cloud.
27
+ * - `inference.*` — credentials used to call a managed inference API
28
+ * (Snowflake Cortex REST API, Databricks AI Gateway). Non-singleton
29
+ * so an org can keep separate dev/prod rows.
30
+ * - reserved: `authoring.*`, `runtime.*`, `connection.*`, `pat.*`,
31
+ * `trust.*`, `generator.*`.
32
+ *
33
+ * Trust and generator payload variants don't appear in the union yet —
34
+ * those land in the followup PR alongside their registry entries. The
35
+ * schema (and the CHECK constraint allowing `class IN ('stored', 'trust',
36
+ * 'generator')`) already supports them so the followup is registry-only.
37
+ */
38
+
39
+ /**
40
+ * Secrets shape for `provisioning.snowflake.pat` and
41
+ * `inference.snowflake-cortex.pat`. Snowflake accepts either a
42
+ * programmatic access token (used as the `password` for `snowflake-sdk`,
43
+ * or as a bearer token against the REST API) or an RSA private key used
44
+ * to sign a short-lived JWT. Consumers branch on which shape is present.
45
+ *
46
+ * Note that the dotted label says `pat` even when the keypair branch is
47
+ * used; `pat` here is the identifier of the auth-method *family*,
48
+ * because Snowflake exposes both as a single endpoint that consumes a
49
+ * bearer token (the keypair JWT and the literal PAT are interchangeable
50
+ * at the request level). When a true second auth method lands (e.g.
51
+ * OAuth M2M) it gets its own tuple with a distinct auth_method segment.
52
+ */
53
+ export type SnowflakeSecrets = { pat: string } | { privateKey: string; privateKeyPassphrase?: string };
54
+
55
+ export type ProvisioningLakebasePayload = {
56
+ type: 'provisioning.lakebase.pat';
57
+ secrets: { pat: string };
58
+ metadata: {
59
+ workspaceUrl?: string;
60
+ adminHost?: string;
61
+ adminPort?: number;
62
+ adminUsername?: string;
63
+ sslMode?: string;
64
+ };
65
+ };
66
+
67
+ export type ProvisioningSnowflakePayload = {
68
+ type: 'provisioning.snowflake.pat';
69
+ secrets: SnowflakeSecrets;
70
+ metadata: {
71
+ account: string;
72
+ username: string;
73
+ warehouse?: string;
74
+ role?: string;
75
+ schema?: string;
76
+ databasePrefix?: string;
77
+ };
78
+ };
79
+
80
+ /**
81
+ * Credentials used to call the Snowflake Cortex REST API. Same secret
82
+ * shapes as `provisioning.snowflake.pat` — the REST API accepts either
83
+ * a long-lived PAT as a bearer token or a short-lived JWT signed with
84
+ * an RSA private key. `cardinality: 'multi'` in the registry because
85
+ * orgs will plausibly want separate dev / prod Cortex identities.
86
+ */
87
+ export type InferenceSnowflakeCortexPayload = {
88
+ type: 'inference.snowflake-cortex.pat';
89
+ secrets: SnowflakeSecrets;
90
+ metadata: {
91
+ /** Snowflake account identifier, e.g. `xy12345.us-east-1`. */
92
+ account: string;
93
+ /** Service user whose PAT or public key is installed on the Snowflake side. */
94
+ username: string;
95
+ /** Role the PAT is scoped to / the JWT should assume at call time. */
96
+ role?: string;
97
+ };
98
+ };
99
+
100
+ /**
101
+ * Databricks AI Gateway credential. AI Gateway only accepts Programmatic
102
+ * Access Tokens (PATs) as bearer credentials — there is no keypair or
103
+ * OAuth path comparable to Snowflake Cortex. `workspaceUrl` is validated
104
+ * at the registry with a strict https + known-suffix regex so a typo
105
+ * there can't silently point at the wrong workspace at call time.
106
+ */
107
+ export type InferenceDatabricksAiGatewayPayload = {
108
+ type: 'inference.databricks-ai-gateway.pat';
109
+ secrets: { pat: string };
110
+ metadata: {
111
+ /**
112
+ * Full workspace URL, e.g. `https://dbc-abcd1234-5678.cloud.databricks.com`
113
+ * or `https://adb-1234567890123456.7.azuredatabricks.net`. Must match
114
+ * the strict Databricks-host regex in the registry.
115
+ */
116
+ workspaceUrl: string;
117
+ };
118
+ };
119
+
120
+ export type CredentialPayload =
121
+ | ProvisioningLakebasePayload
122
+ | ProvisioningSnowflakePayload
123
+ | InferenceSnowflakeCortexPayload
124
+ | InferenceDatabricksAiGatewayPayload;
125
+
126
+ export type CredentialType = CredentialPayload['type'];
127
+
128
+ /**
129
+ * All known `<role>` prefixes. Used by callers to filter credentials by
130
+ * role (e.g. "list all provisioning credentials for this org"). Includes
131
+ * reserved-but-unused roles so adding a new one later is a registry +
132
+ * union change rather than a cross-cutting rename.
133
+ *
134
+ * `trust` and `generator` are reserved for the credential-class taxonomy
135
+ * (see `CredentialClass` below): trust-config rows like
136
+ * `trust.aws.iam-role-assume` will claim `role: 'trust'`, generator rows
137
+ * like `generator.snowflake.keypair-jwt` will claim `role: 'generator'`.
138
+ * They are not yet populated in the registry — these reserved values
139
+ * just keep the `(class, role, backend, auth_method)` tuple consistent
140
+ * so the followup PR's registrations are a code-only change.
141
+ */
142
+ export const CREDENTIAL_ROLES = ['provisioning', 'authoring', 'runtime', 'connection', 'inference', 'pat', 'trust', 'generator'] as const;
143
+ export type CredentialRole = (typeof CREDENTIAL_ROLES)[number];
144
+
145
+ /**
146
+ * Three credential classes, recorded on every row of the `credential`
147
+ * table:
148
+ * - `stored` — encrypted secret bytes are the credential
149
+ * (PATs, RSA private keys held for JWT signing).
150
+ * - `trust` — the credential is a trust assertion held in
151
+ * `metadata`; `encrypted_secrets` is NULL. The DB's
152
+ * `chk_credential_secrets_class` CHECK constraint
153
+ * enforces this.
154
+ * - `generator` — encrypted bytes are *generator material* (OAuth
155
+ * client_id+secret, RSA keypair) used to mint a
156
+ * short-lived runtime token. The minted token is
157
+ * never persisted; only the cache key tuple
158
+ * (orgId, credentialId, fingerprint, derivationKey)
159
+ * is.
160
+ *
161
+ * The class is denormalized onto the row so query paths can branch on
162
+ * it without joining the registry, and so the schema CHECK constraint
163
+ * can enforce the secrets-vs-no-secrets invariant. Only `'stored'`
164
+ * entries are registered today; `'trust'` and `'generator'` are
165
+ * reserved by the schema and the type system, awaiting their registry
166
+ * additions in the followup PR.
167
+ */
168
+ export const CREDENTIAL_CLASSES = ['stored', 'trust', 'generator'] as const;
169
+ export type CredentialClass = (typeof CREDENTIAL_CLASSES)[number];
170
+
171
+ /**
172
+ * The process where a credential's plaintext (or its derived runtime
173
+ * token, for generator credentials) may legitimately exist.
174
+ *
175
+ * - `server` — the Superblocks control plane.
176
+ * - `orchestrator` — the customer's On-Premise Agent.
177
+ * - `worker` — the orchestrator's per-language sandboxed
178
+ * worker process (Go/JS/Python).
179
+ * - `customer-sm` — held in the customer's own secret manager
180
+ * (AWS Secrets Manager, GCP Secret Manager,
181
+ * Vault); the Superblocks control plane never
182
+ * touches it. Reserved.
183
+ *
184
+ * The repository's `assertProcessCanHandle` guard reads a process-level
185
+ * `PROCESS_LOCATION` constant and rejects with
186
+ * `WrongProcessForCredentialError` if a row's
187
+ * `runtimePlaintextLocation` excludes the current process. Replaces
188
+ * the implicit "this code path lives server-side, so the secret must
189
+ * be decryptable here" convention with a typed check.
190
+ */
191
+ export const RUNTIME_PLAINTEXT_LOCATIONS = ['server', 'orchestrator', 'worker', 'customer-sm'] as const;
192
+ export type RuntimePlaintextLocation = (typeof RUNTIME_PLAINTEXT_LOCATIONS)[number];
193
+
194
+ /**
195
+ * Format a `(role, backend, authMethod)` tuple into the dotted
196
+ * `CredentialType` label that clients see on the wire. The schema
197
+ * stores the tuple; this is the canonical projection for DTOs and
198
+ * registry-by-label lookups. Always 3 segments.
199
+ */
200
+ export function formatCredentialType(role: string, backend: string, authMethod: string): CredentialType {
201
+ return `${role}.${backend}.${authMethod}` as CredentialType;
202
+ }
203
+
204
+ /**
205
+ * Inverse of `formatCredentialType`. Returns `null` when the input
206
+ * doesn't have exactly three dotted segments — the caller decides
207
+ * whether that's a hard error or a soft signal. Backend and auth-method
208
+ * identifiers may contain hyphens but never dots, so a length-3 split
209
+ * is unambiguous.
210
+ */
211
+ export function parseCredentialType(type: string): { role: string; backend: string; authMethod: string } | null {
212
+ const parts = type.split('.');
213
+ if (parts.length !== 3) {
214
+ return null;
215
+ }
216
+ return { role: parts[0], backend: parts[1], authMethod: parts[2] };
217
+ }
218
+
219
+ /**
220
+ * Convenience: extract the role segment of a `CredentialType`. The
221
+ * full tuple round-trip is `parseCredentialType`.
222
+ */
223
+ export function credentialRole(type: CredentialType): CredentialRole {
224
+ return type.split('.')[0] as CredentialRole;
225
+ }
226
+
227
+ /**
228
+ * Non-secret projection returned from list / GET endpoints. Never contains
229
+ * `secrets`. `metadata` is the same shape the client POSTed under that key.
230
+ *
231
+ * `class`, `role`, `backend`, `authMethod`, and `runtimePlaintextLocation`
232
+ * come from columns on the row (the canonical tuple plus the registry-
233
+ * pinned class and process-location). `type` is the dotted label
234
+ * projected from `(role, backend, authMethod)` — kept on the DTO as
235
+ * an API-stable identifier so existing clients don't have to assemble
236
+ * it themselves.
237
+ */
238
+ export interface CredentialDto {
239
+ id: string;
240
+ organizationId: string;
241
+ /** Derived label `${role}.${backend}.${authMethod}`; not stored. */
242
+ type: CredentialType;
243
+ /** `null` when the registry marks the tuple as a singleton. */
244
+ name: string | null;
245
+ metadata: CredentialPayload['metadata'];
246
+ /** First 8 hex of SHA-256 over the decrypted secrets object at write time. */
247
+ fingerprint: string;
248
+ /** See `CredentialClass`. Pinned by the registry on write. */
249
+ class: CredentialClass;
250
+ /** See `CREDENTIAL_ROLES`. First segment of the canonical tuple. */
251
+ role: CredentialRole;
252
+ /** Short backend identifier, e.g. `lakebase`, `snowflake`, `aws`. */
253
+ backend: string;
254
+ /** Short auth-method identifier, e.g. `pat`, `keypair`, `oauth-m2m`. */
255
+ authMethod: string;
256
+ /** See `RuntimePlaintextLocation`. Pinned by the registry on write. */
257
+ runtimePlaintextLocation: RuntimePlaintextLocation;
258
+ createdAt: string;
259
+ updatedAt: string;
260
+ createdBy: string | null;
261
+ }
262
+
263
+ export interface UpsertCredentialRequest {
264
+ type: CredentialType;
265
+ name?: string | null;
266
+ secrets: CredentialPayload['secrets'];
267
+ metadata: CredentialPayload['metadata'];
268
+ }
@@ -9,6 +9,7 @@ export * from './ai/index.js';
9
9
  export * from './commit/index.js';
10
10
  export * from './branch/index.js';
11
11
  export * from './common/index.js';
12
+ export * from './credentials/index.js';
12
13
  export * from './datasource/index.js';
13
14
  export * from './degradedMode/index.js';
14
15
  export * from './deployment/index.js';
@@ -23,6 +23,22 @@ export const SUPERBLOCKS_SUPPORT_EMAIL = 'support@superblocks.com';
23
23
  export const VISITOR_ORG_NAME = 'visitor';
24
24
  export const VISITOR_ORG_ID = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa';
25
25
 
26
+ /**
27
+ * Snapshot of the org's app-inference configuration, embedded on `/me` so the UI
28
+ * bootstrap gets it without a second round trip to
29
+ * `/api/v1/organizations/:id/app-inference-configuration`.
30
+ *
31
+ * Tri-state semantics on `Organization.appInferenceConfiguration`:
32
+ * - `undefined` → feature unavailable (gate flag off for the org)
33
+ * - `null` → feature available, admin has not configured anything
34
+ * - populated → feature available, admin has configured a preferred integration
35
+ */
36
+ export interface OrganizationAppInferenceConfiguration {
37
+ preferredIntegrationId: string;
38
+ updatedBy: string | null;
39
+ updated: string;
40
+ }
41
+
26
42
  export interface Organization {
27
43
  id: string;
28
44
  name: string;
@@ -37,6 +53,7 @@ export interface Organization {
37
53
  profiles?: Profile[];
38
54
  created: Date;
39
55
  version?: OrgVersionType;
56
+ appInferenceConfiguration?: OrganizationAppInferenceConfiguration | null;
40
57
  }
41
58
 
42
59
  export type OrgBriefDto = {
@@ -141,6 +141,7 @@ export enum ResourceTypeEnum {
141
141
  GROUPS = 'groups',
142
142
  GROUPS_MEMBERS = 'groups.members',
143
143
  INTEGRATIONS = 'integrations',
144
+ INTEGRATIONS_DEFAULT_AI = 'integrations.default_ai',
144
145
  LOGS = 'logs',
145
146
  LOGS_STREAMS = 'logs.streams',
146
147
  ORGANIZATION = 'org',
@@ -189,6 +190,9 @@ export const ActionTypeByResourceType = {
189
190
  READ: ActionTypeEnum.READ,
190
191
  MANAGE: ActionTypeEnum.MANAGE
191
192
  },
193
+ INTEGRATIONS_DEFAULT_AI: {
194
+ MANAGE: ActionTypeEnum.MANAGE
195
+ },
192
196
  BILLING: {
193
197
  MANAGE: ActionTypeEnum.MANAGE
194
198
  },