@centrali-io/centrali-mcp 4.4.5 → 4.4.6

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.
@@ -82,6 +82,9 @@ function registerDescribeTools(server) {
82
82
  "list_allowed_domains",
83
83
  "add_allowed_domain",
84
84
  "remove_allowed_domain",
85
+ "get_function_run",
86
+ "list_function_runs",
87
+ "get_compute_job_status",
85
88
  ],
86
89
  },
87
90
  smart_queries: {
@@ -161,8 +164,119 @@ function registerDescribeTools(server) {
161
164
  "accept_page_proposal",
162
165
  ],
163
166
  },
167
+ auth_providers: {
168
+ summary: "External identity provider configuration for BYOT (Bring Your Own Token). Connect Clerk, Auth0, Okta, Keycloak, or any OIDC provider so your app's users can authenticate with their existing IdP and Centrali handles authorization via claim-based policies.",
169
+ describeWith: "describe_auth_providers",
170
+ tools: [
171
+ "list_auth_providers",
172
+ "get_auth_provider",
173
+ "create_auth_provider",
174
+ "update_auth_provider",
175
+ "delete_auth_provider",
176
+ "test_auth_provider",
177
+ "refresh_auth_provider_jwks",
178
+ ],
179
+ },
180
+ service_accounts: {
181
+ summary: "Machine identities for backend-to-backend API access. Create service accounts with client_credentials OAuth2 flow, manage roles and groups for fine-grained permissions, and introspect access with permission scanning.",
182
+ describeWith: "describe_service_accounts",
183
+ tools: [
184
+ "list_service_accounts",
185
+ "get_service_account",
186
+ "create_service_account",
187
+ "update_service_account_name",
188
+ "update_service_account_description",
189
+ "delete_service_account",
190
+ "rotate_service_account_secret",
191
+ "revoke_service_account",
192
+ "generate_dev_token",
193
+ "scan_service_account_permissions",
194
+ "simulate_service_account_permission",
195
+ "generate_remediation",
196
+ "preview_remediation",
197
+ "apply_remediation",
198
+ "list_service_account_roles",
199
+ "assign_role_to_service_account",
200
+ "remove_role_from_service_account",
201
+ "list_service_account_groups",
202
+ "add_service_account_to_group",
203
+ "remove_service_account_from_group",
204
+ "list_roles",
205
+ "get_role",
206
+ "create_role",
207
+ "update_role",
208
+ "delete_role",
209
+ "list_groups",
210
+ "get_group",
211
+ "create_group",
212
+ "update_group",
213
+ "delete_group",
214
+ "list_publishable_keys",
215
+ "get_publishable_key",
216
+ "create_publishable_key",
217
+ "update_publishable_key",
218
+ "revoke_publishable_key",
219
+ "get_current_identity",
220
+ ],
221
+ },
222
+ },
223
+ workflow: "Typical workflow: 1) Define collections → 2) Create records → 3) Write compute functions → 4) Wire orchestrations → 5) Build pages to surface data → 6) Publish pages for end users. When building an app, also: 7) Create a service account → 8) Grant least-privilege permissions via remediation → 9) Create publishable keys for the frontend.",
224
+ app_credential_setup: {
225
+ description: "When building an app that uses Centrali, you need credentials. The flow depends on whether the code runs server-side or client-side.",
226
+ backend: {
227
+ description: "Server-side apps (Next.js API routes, Node.js services) use service accounts.",
228
+ flow: [
229
+ "1. create_service_account → get clientId + clientSecret",
230
+ "2. scan_service_account_permissions(filter='denied') → see what's missing",
231
+ "3. generate_remediation(sa, resource, category, actions) → get options to grant access",
232
+ "4. Pick the LEAST-PRIVILEGE option (prefer 'create_new' for exact permissions over broad role assignments)",
233
+ "5. apply_remediation → grant access",
234
+ "6. Use clientId/clientSecret in the SDK: new CentraliSDK({ clientId, clientSecret, ... })",
235
+ ],
236
+ principle: "ALWAYS grant least privilege. Only grant the exact resources and actions the app needs. Never assign a broad admin role when a narrow permission will do.",
237
+ },
238
+ frontend: {
239
+ description: "Client-side apps (React, Vue, mobile) use publishable keys — safe to embed in code.",
240
+ flow: [
241
+ "1. create_publishable_key with scoped access (e.g., records:list:products, records:retrieve:products)",
242
+ "2. Use the key in the SDK: new CentraliSDK({ publishableKey: 'pk_live_...', ... })",
243
+ ],
244
+ principle: "Publishable keys are already scoped. Only grant the specific collections and actions the UI needs. Write actions (create, execute) require explicit collection slugs — no wildcards.",
245
+ },
246
+ },
247
+ docs: "Full documentation: https://docs.centrali.io — SDK guide, API reference, compute functions, orchestrations, and more.",
248
+ feature_matrix: {
249
+ description: "Not all features are available through all interfaces. Use this matrix to know which tool to use.",
250
+ capabilities: {
251
+ "Collections CRUD": { sdk: true, mcp: true, console: true },
252
+ "Records CRUD": { sdk: true, mcp: true, console: true },
253
+ "Compute functions": { sdk: true, mcp: true, console: true },
254
+ "Triggers": { sdk: true, mcp: true, console: true },
255
+ "Smart queries": { sdk: true, mcp: true, console: true },
256
+ "Orchestrations": { sdk: true, mcp: true, console: true },
257
+ "Pages": { sdk: "partial", mcp: true, console: true },
258
+ "Realtime subscriptions (SSE)": { sdk: true, mcp: false, console: false, note: "SDK-only — use client.realtime.subscribe() in app code" },
259
+ "Validation scans": { sdk: true, mcp: true, console: true },
260
+ "Anomaly insights": { sdk: true, mcp: true, console: true },
261
+ "Search": { sdk: true, mcp: true, console: true },
262
+ "Function runs (execution history)": { sdk: true, mcp: true, console: true },
263
+ "Service accounts & IAM": { sdk: false, mcp: true, console: true, note: "MCP tools for full SA lifecycle, roles, groups, and permission introspection" },
264
+ "File uploads": { sdk: true, mcp: false, console: true, note: "SDK-only — use client.uploadFile() in app code" },
265
+ },
266
+ },
267
+ sdk_integration: {
268
+ description: "When building an app, use MCP tools to create backend resources (collections, functions, triggers, orchestrations) and the SDK in your app code to interact with them at runtime.",
269
+ install: "npm install @centrali-io/centrali-sdk",
270
+ nextjs_server: {
271
+ description: "For Next.js API routes and server components, use service account auth.",
272
+ code: "import { CentraliSDK } from '@centrali-io/centrali-sdk';\nconst centrali = new CentraliSDK({ baseUrl: 'https://centrali.io', workspaceId: 'your-workspace-slug', clientId: process.env.CENTRALI_CLIENT_ID, clientSecret: process.env.CENTRALI_CLIENT_SECRET });",
273
+ },
274
+ react_vite_client: {
275
+ description: "For React + Vite browser apps, use a publishable key (safe to expose in client code).",
276
+ code: "import { CentraliSDK } from '@centrali-io/centrali-sdk';\nconst centrali = new CentraliSDK({ baseUrl: 'https://centrali.io', workspaceId: 'your-workspace-slug', publishableKey: 'pk_live_your_key_here' });",
277
+ },
278
+ note: "workspaceId is the workspace slug (e.g., 'acme'), not a UUID. See https://docs.centrali.io/guides/centrali-sdk for the full guide.",
164
279
  },
165
- workflow: "Typical workflow: 1) Define collections → 2) Create records → 3) Write compute functions → 4) Wire orchestrations → 5) Build pages to surface data → 6) Publish pages for end users.",
166
280
  }, null, 2),
167
281
  },
168
282
  ],
@@ -505,26 +619,93 @@ function registerDescribeTools(server) {
505
619
  wildcards: "Use '*.example.com' to allow all subdomains of example.com",
506
620
  note: "Functions can only make HTTP requests to domains on this list. Add domains before creating functions that call external APIs.",
507
621
  },
622
+ compute_input_contract: {
623
+ description: "Every compute function has three globals: api, triggerParams, executionParams. What they contain depends on how the function was invoked.",
624
+ signature: "async function run() { /* use api, triggerParams, executionParams */ return result; }",
625
+ important: "Do NOT use module.exports. Always use 'async function run() { ... }'.",
626
+ per_trigger_type: {
627
+ "http-trigger": {
628
+ triggerParams: "Static params from trigger.triggerMetadata.params (set at trigger creation time)",
629
+ executionParams: "{ payload } — parsed request body from the incoming HTTP POST",
630
+ },
631
+ "endpoint": {
632
+ triggerParams: "Static params from trigger.triggerMetadata.params",
633
+ executionParams: "{ payload, method, headers, query } — full HTTP request context",
634
+ },
635
+ "scheduled": {
636
+ triggerParams: "Static params from trigger.triggerMetadata.params (max 64KB)",
637
+ executionParams: "{} — empty (no request context for scheduled runs)",
638
+ },
639
+ "on-demand": {
640
+ triggerParams: "Static params from trigger.triggerMetadata.params",
641
+ executionParams: "{ payload } — the payload passed to invoke_trigger",
642
+ },
643
+ "pages-action": {
644
+ triggerParams: "{ input, token } — from page action invocation",
645
+ executionParams: "{} — empty",
646
+ },
647
+ "orchestration-step": {
648
+ triggerParams: "Merged: orchestration input + previous step outputs + decrypted encrypted params",
649
+ executionParams: "Not used — all input arrives via triggerParams for orchestration steps",
650
+ },
651
+ },
652
+ },
508
653
  sandbox_api: {
509
- description: "Functions receive an `api` object with built-in utilities. Key crypto methods:",
654
+ description: "Functions receive an `api` object with built-in utilities.",
655
+ records: "api.queryRecords, api.fetchRecord, api.createRecord, api.updateRecord, api.deleteRecord, api.bulkCreateRecords, api.bulkUpdateRecords, api.bulkDeleteRecords",
656
+ http: "api.httpRequest, api.httpFetch — outbound calls to allowed domains only",
657
+ files: "api.storeFile, api.storeAsCSV, api.storeAsJSON",
510
658
  crypto: {
511
659
  "api.crypto.sha256(data)": "SHA256 hash → base64 string",
512
660
  "api.crypto.hmacSha256(key, data, options?)": "HMAC-SHA256 → base64 string. options: { keyEncoding: 'utf8' | 'base64' }",
513
661
  "api.crypto.rsaSign(privateKeyPem, data, algorithm?)": "RSA signature → base64url string. algorithm: 'RS256' (default), 'RS384', 'RS512'",
514
662
  "api.crypto.signJwt(privateKeyPem, payload, options?)": "Build + sign a JWT → 'header.payload.signature'. options: { algorithm, expiresIn (seconds), issuer }",
515
663
  },
516
- note: "Private keys should be stored as encrypted trigger params. signJwt automatically sets iat and supports RS256/RS384/RS512.",
664
+ utilities: "api.uuid, api.formatDate, api.chunk, api.merge, api.math, api.evaluate, api.renderTemplate, api.log, api.logError, api.toCSV, api.toJSON",
665
+ },
666
+ secrets: {
667
+ description: "Compute functions have NO built-in environment variables or secrets field.",
668
+ recommended: "Wrap the function in an orchestration and use encrypted params on the compute step. Encrypted params are stored with AES-256-GCM at rest, decrypted at execution time, and arrive in triggerParams.",
669
+ alternative: "Pass secrets in the trigger invocation payload via executionParams. This works but means the calling app is the courier for the secret.",
670
+ encrypted_params_flow: "Create orchestration → add compute step with encryptedParams: { API_KEY: { value: 'secret', encrypt: true } } → at execution, API_KEY arrives decrypted in triggerParams.API_KEY",
671
+ },
672
+ async_execution: {
673
+ description: "Trigger invocation is ASYNCHRONOUS. invoke_trigger returns a job ID, not the execution result. There are two ways to get the result.",
674
+ path_1_realtime: {
675
+ description: "SDK realtime subscription (recommended for apps). Subscribe to run status events via SSE — no polling needed. The SDK handles reconnection automatically.",
676
+ when_to_use: "In your app code (Next.js, React, Node.js server). Best for dashboards and UIs that need to show run progress in real-time.",
677
+ how: "Use client.realtime.subscribe() in your app to listen for function run completion events keyed by the jobId.",
678
+ note: "Realtime is SDK-only — not available through MCP.",
679
+ },
680
+ path_2_polling: {
681
+ description: "Poll with get_compute_job_status(jobId) (recommended for MCP and scripts).",
682
+ when_to_use: "In MCP tool calls, CLI scripts, or any context where you can't hold a realtime connection.",
683
+ flow: "1) Call invoke_trigger → get jobId. 2) Call get_compute_job_status(jobId) to check state. 3) Repeat until state is 'completed' or 'failed'.",
684
+ },
685
+ job_statuses: "queued → running → completed | failed",
686
+ tools: {
687
+ get_compute_job_status: "Poll by job ID — returns state, returnValue (on success), failedReason (on failure). This is the direct way to check a specific invocation.",
688
+ get_function_run: "Get a function run by its run UUID — returns detailed status, output (runData), errors, timing, memory/CPU usage",
689
+ list_function_runs: "List runs by triggerId or functionId — useful for browsing execution history",
690
+ },
691
+ tip: "For orchestration workflows, use get_orchestration_run(includeSteps=true) instead — it shows step-by-step execution with inputs/outputs.",
692
+ },
693
+ success_vs_failure: {
694
+ description: "Compute functions can fail in two ways. The distinction matters for debugging.",
695
+ throw: "Throwing an error → run status = 'failure', errorMessage populated. Use for unexpected errors.",
696
+ return_error: "Returning { ok: false, ... } → run status = 'completed', but the business action failed. Use for expected failures (e.g., provider returned 403).",
697
+ recommendation: "For provider-backed calls (Resend, Stripe, etc.), check the response status and return { ok: false, status, body } so the caller can diagnose. Only throw for truly unexpected errors.",
517
698
  },
518
699
  tips: [
519
700
  "Use list_functions to see all available functions",
520
701
  "Use list_triggers to see how functions are wired to execution events",
521
702
  "Only on-demand triggers can be invoked via invoke_trigger — other types fire automatically",
522
- "Functions receive the trigger payload in their execution context",
523
- "Use pause_trigger/resume_trigger to temporarily disable triggers without deleting them",
524
703
  "Use test_function to validate code before creating or updating a function",
525
704
  "Use create_function + create_trigger together to set up a complete compute pipeline",
705
+ "Use get_function_run or list_function_runs to check execution results after invoking a trigger",
526
706
  "Use api.crypto.signJwt() for GitHub App, Google Cloud, or Azure AD authentication flows",
527
707
  "Before creating functions that call external APIs, add the target domains with add_allowed_domain",
708
+ "For secrets (API keys, credentials), use orchestration encrypted params — see the 'secrets' section above",
528
709
  ],
529
710
  }, null, 2),
530
711
  },
@@ -1675,4 +1856,317 @@ function registerDescribeTools(server) {
1675
1856
  ],
1676
1857
  });
1677
1858
  }));
1859
+ // ── Auth Providers (BYOT) ──────────────────────────────────────────
1860
+ server.tool("describe_auth_providers", "Get the full reference for configuring external auth providers (BYOT — Bring Your Own Token). Explains how to connect Clerk, Auth0, Okta, or any OIDC provider so your app's users authenticate with their existing IdP while Centrali handles authorization.", {}, () => __awaiter(this, void 0, void 0, function* () {
1861
+ return ({
1862
+ content: [
1863
+ {
1864
+ type: "text",
1865
+ text: JSON.stringify({
1866
+ domain: "External Auth Providers (BYOT)",
1867
+ description: "BYOT (Bring Your Own Token) lets your app keep using Clerk, Auth0, Okta, or any OIDC provider for authentication while Centrali handles fine-grained authorization. No user duplication, no token exchange — just pass the JWT.",
1868
+ how_it_works: {
1869
+ step1: "User authenticates via your IdP (Clerk, Auth0, Okta, etc.) → receives a JWT",
1870
+ step2: "Your app passes the JWT to Centrali in the Authorization header",
1871
+ step3: "Centrali validates the JWT signature via the provider's JWKS (auto-discovered and cached)",
1872
+ step4: "Centrali extracts claims using your configured claim mappings (e.g., org_role → ext_role)",
1873
+ step5: "Authorization policies evaluate using extracted claims (ext_* attributes)",
1874
+ step6: "Centrali returns Allow or Deny",
1875
+ },
1876
+ blog_guide: "https://centrali.io/blog/byot-bring-your-own-token-external-idp",
1877
+ docs: "https://docs.centrali.io/guides/external-auth",
1878
+ provider_types: {
1879
+ clerk: "Clerk — auto-discovers JWKS from issuer URL (https://clerk.your-domain.com)",
1880
+ auth0: "Auth0 — auto-discovers JWKS from issuer URL (https://your-tenant.auth0.com/)",
1881
+ okta: "Okta — auto-discovers JWKS from issuer URL (https://your-org.okta.com)",
1882
+ keycloak: "Keycloak — auto-discovers JWKS from issuer URL",
1883
+ oidc: "Any OIDC-compliant provider — auto-discovers JWKS from issuer's .well-known/openid-configuration",
1884
+ custom: "Custom JWT issuer — requires manual jwksUrl",
1885
+ },
1886
+ provider_shape: {
1887
+ id: "UUID — auto-generated",
1888
+ name: "string — display name",
1889
+ slug: "string — URL-safe unique identifier",
1890
+ providerType: "'oidc' | 'clerk' | 'auth0' | 'keycloak' | 'okta' | 'custom'",
1891
+ issuer: "string (URL) — JWT issuer. Must match the 'iss' claim in tokens. Unique per workspace.",
1892
+ jwksUrl: "string (URL) | null — override JWKS URL (usually auto-discovered from issuer)",
1893
+ allowedAudiences: "string[] — if set, tokens must include a matching 'aud' claim",
1894
+ clockSkewSeconds: "number — tolerance for token expiration (default: 60, max: 300)",
1895
+ allowedAlgorithms: "string[] — accepted signing algorithms (default: ['RS256'])",
1896
+ claimMappings: "ClaimMapping[] — defines how JWT claims map to policy attributes",
1897
+ allowedOrigins: "string[] — CORS origins for browser requests",
1898
+ isActive: "boolean — inactive providers reject all tokens",
1899
+ },
1900
+ claim_mappings: {
1901
+ description: "Claim mappings extract values from JWT tokens and make them available as ext_<attribute> in authorization policies.",
1902
+ shape: {
1903
+ attribute: "string — becomes ext_<attribute> in policies. Example: 'role' → ext_role. Must start with a letter, alphanumeric + underscores only. Do NOT include the ext_ prefix — it's added automatically.",
1904
+ jwtPath: "string — dot-notation path into the JWT payload. Examples: 'org_role', 'metadata.plan', 'organization.role'",
1905
+ required: "boolean (default: false) — if true, token validation fails when this claim is missing",
1906
+ defaultValue: "string | number | boolean | string[] — fallback when claim is absent",
1907
+ transform: "'lowercase' | 'uppercase' | 'string' | 'boolean' | 'array' — applied to extracted value",
1908
+ },
1909
+ examples: [
1910
+ { attribute: "role", jwtPath: "org_role", description: "Maps JWT org_role claim → ext_role in policies" },
1911
+ { attribute: "plan", jwtPath: "metadata.plan", description: "Maps nested metadata.plan → ext_plan" },
1912
+ { attribute: "department", jwtPath: "user.public_metadata.department", defaultValue: "general", description: "Deep path with fallback" },
1913
+ ],
1914
+ },
1915
+ clerk_setup: {
1916
+ description: "Step-by-step guide for Clerk integration.",
1917
+ steps: [
1918
+ "1. In Clerk Dashboard → Configure → JWT Templates, create a template named 'centrali' with the claims you need",
1919
+ "2. Example JWT template: { \"org_id\": \"{{org.id}}\", \"org_role\": \"{{org.role}}\", \"metadata\": { \"plan\": \"{{user.public_metadata.plan}}\" } }",
1920
+ "3. Create the auth provider in Centrali: create_auth_provider with providerType='clerk', issuer='https://clerk.your-domain.com'",
1921
+ "4. Add claim mappings: [{ attribute: 'role', jwtPath: 'org_role' }, { attribute: 'plan', jwtPath: 'metadata.plan' }]",
1922
+ "5. In your Next.js app, get the token: const token = await getToken({ template: 'centrali' })",
1923
+ "6. Pass the token to Centrali SDK: centrali.checkAuthorization({ token, resource: 'orders', action: 'approve' })",
1924
+ ],
1925
+ nextjs_example: "import { auth } from '@clerk/nextjs/server';\nimport { CentraliSDK } from '@centrali-io/centrali-sdk';\n\nconst centrali = new CentraliSDK({ baseUrl: 'https://centrali.io', workspaceId: 'your-workspace-slug', });\n\nexport async function POST(request: Request) {\n const { getToken } = await auth();\n const token = await getToken({ template: 'centrali' });\n const result = await centrali.checkAuthorization({ token, resource: 'premium-features', action: 'access' });\n if (!result.data.allowed) return Response.json({ error: 'Upgrade to premium' }, { status: 403 });\n return Response.json({ data: '...' });\n}",
1926
+ },
1927
+ policy_attributes: {
1928
+ description: "Attributes available in authorization policies when using external tokens.",
1929
+ attributes: {
1930
+ "ext_*": "Claims extracted via claim mappings (e.g., ext_role, ext_plan)",
1931
+ "is_external_principal": "Always true for external tokens",
1932
+ "external_issuer": "The JWT issuer URL",
1933
+ "external_subject": "The JWT subject (sub) claim",
1934
+ "request_metadata.*": "Context values passed via the context parameter in checkAuthorization()",
1935
+ },
1936
+ },
1937
+ policy_example: {
1938
+ description: "Example policy that allows premium users to access a feature.",
1939
+ policy: {
1940
+ name: "premium_access",
1941
+ specification: {
1942
+ rules: [
1943
+ {
1944
+ rule_id: "premium-allow",
1945
+ effect: "Allow",
1946
+ conditions: [
1947
+ { function: "string_equal", attribute: "ext_plan", value: "premium" },
1948
+ ],
1949
+ },
1950
+ ],
1951
+ default: { effect: "Deny" },
1952
+ },
1953
+ },
1954
+ },
1955
+ tools: {
1956
+ list_auth_providers: "List all configured providers",
1957
+ get_auth_provider: "Get provider details including claim mappings",
1958
+ create_auth_provider: "Create a new provider (Clerk, Auth0, Okta, OIDC, etc.)",
1959
+ update_auth_provider: "Update provider config (mappings, audiences, active status)",
1960
+ delete_auth_provider: "Delete a provider (tokens will be rejected)",
1961
+ test_auth_provider: "Test claim extraction with a sample JWT — validates your mappings without affecting production",
1962
+ refresh_auth_provider_jwks: "Force refresh JWKS cache after key rotation",
1963
+ },
1964
+ tips: [
1965
+ "Call create_auth_provider first, then test_auth_provider with a real JWT to validate claim mappings before deploying",
1966
+ "Claim attributes are automatically prefixed with ext_ — if you map attribute 'role', it becomes ext_role in policies",
1967
+ "JWKS is auto-discovered from the issuer URL for standard providers — you rarely need to set jwksUrl manually",
1968
+ "Use test_auth_provider to debug claim extraction without affecting production traffic",
1969
+ "Use allowedAudiences to restrict which apps can use this provider (matches the JWT 'aud' claim)",
1970
+ "Set isActive to false to temporarily disable a provider without deleting it",
1971
+ "For Clerk: create a JWT template in the Clerk Dashboard and retrieve tokens with getToken({ template: 'centrali' })",
1972
+ ],
1973
+ }, null, 2),
1974
+ },
1975
+ ],
1976
+ });
1977
+ }));
1978
+ // ── Service Accounts & IAM ──────────────────────────────────────
1979
+ server.tool("describe_service_accounts", "Get the full reference for managing service accounts, roles, groups, and permissions. Explains the complete IAM setup flow from creating a service account to granting fine-grained access.", {}, () => __awaiter(this, void 0, void 0, function* () {
1980
+ return ({
1981
+ content: [
1982
+ {
1983
+ type: "text",
1984
+ text: JSON.stringify({
1985
+ domain: "Service Accounts & IAM",
1986
+ description: "Service accounts are machine identities for backend-to-backend API access. They authenticate via OAuth2 client_credentials flow and receive permissions through roles and groups.",
1987
+ setup_flow: {
1988
+ description: "End-to-end flow for setting up a service account for a new app with least-privilege permissions.",
1989
+ steps: [
1990
+ "1. Create a service account: create_service_account — returns clientId + clientSecret (secret shown ONCE)",
1991
+ "2. Identify what the app needs: which collections it reads/writes, which triggers it invokes, which files it accesses",
1992
+ "3. Grant exactly those permissions: generate_remediation → pick the LEAST-PRIVILEGE option (prefer 'create_new' for exact access) → apply_remediation",
1993
+ "4. Verify: scan_service_account_permissions(filter='denied') — the only denied items should be things the app doesn't need",
1994
+ "5. Use the credentials: new CentraliSDK({ clientId, clientSecret, ... }) in your app",
1995
+ ],
1996
+ least_privilege_principle: "ALWAYS grant the minimum permissions needed. When generate_remediation returns multiple options, prefer 'create_new' (creates a minimal policy for exactly the requested access) over 'role_assignment' (which may grant broader access than needed). Only use role_assignment if the existing role exactly matches what you need.",
1997
+ },
1998
+ authentication: {
1999
+ production: {
2000
+ description: "OAuth2 client_credentials flow — standard, secure, auto-refreshing.",
2001
+ flow: "POST /iam/oauth2/token with grant_type=client_credentials, client_id, client_secret → receives JWT access token",
2002
+ sdk_example: "const centrali = new CentraliSDK({ baseUrl: 'https://centrali.io', workspaceId: 'acme', clientId: 'ci_...', clientSecret: 'sk_...' }); // SDK handles token refresh automatically",
2003
+ },
2004
+ development: {
2005
+ description: "Dev tokens — short-lived tokens for testing without the OAuth2 flow.",
2006
+ tool: "generate_dev_token(serviceAccountId, ttlSeconds)",
2007
+ note: "Dev tokens have limited TTL. For production, always use client_credentials flow.",
2008
+ },
2009
+ },
2010
+ service_account_shape: {
2011
+ id: "number — auto-generated numeric ID",
2012
+ clientId: "string — format: ci_<hex> — used as the client_id in OAuth2",
2013
+ clientSecret: "string — format: sk_<hex> — ONLY returned on creation and rotation",
2014
+ name: "string — display name",
2015
+ description: "string | null — optional description",
2016
+ workspaceSlug: "string — workspace this SA belongs to",
2017
+ revoked: "boolean — if true, all tokens are invalid and no new tokens can be issued",
2018
+ createdBy: "string — user ID who created this SA",
2019
+ createdAt: "string — ISO 8601 timestamp",
2020
+ },
2021
+ permission_model: {
2022
+ description: "Permissions flow: Service Account → Roles → Permissions. Groups are optional bundles of SAs.",
2023
+ role: {
2024
+ description: "A named bundle of permissions. Roles are assigned directly to SAs or inherited through groups.",
2025
+ shape: {
2026
+ id: "UUID",
2027
+ name: "string (e.g., 'Data Reader', 'Compute Admin')",
2028
+ description: "string | null",
2029
+ permissions: "string[] — permission UUIDs",
2030
+ },
2031
+ },
2032
+ group: {
2033
+ description: "A named bundle of service accounts. Roles assigned to a group are inherited by all SAs in it.",
2034
+ shape: {
2035
+ id: "UUID",
2036
+ name: "string (e.g., 'Backend Services')",
2037
+ description: "string | null",
2038
+ },
2039
+ },
2040
+ permission_actions: [
2041
+ "create — create new resources",
2042
+ "retrieve — read a single resource by ID",
2043
+ "list — list/query resources",
2044
+ "update — modify existing resources",
2045
+ "delete — remove resources",
2046
+ "execute — run compute functions, invoke triggers",
2047
+ "manage — administrative actions (e.g., generate dev tokens)",
2048
+ ],
2049
+ common_resources: [
2050
+ "workspace::records",
2051
+ "workspace::structures",
2052
+ "workspace::compute-functions",
2053
+ "workspace::function-triggers",
2054
+ "workspace::orchestrations",
2055
+ "workspace::smart-queries",
2056
+ "workspace::pages",
2057
+ "workspace::service-accounts",
2058
+ "workspace::roles",
2059
+ "workspace::groups",
2060
+ "workspace::files",
2061
+ "workspace::search",
2062
+ ],
2063
+ },
2064
+ introspection: {
2065
+ scan: "scan_service_account_permissions — full access matrix showing every resource/action with Allow/Deny and reasons",
2066
+ simulate: "simulate_service_account_permission — test a specific resource+action, get evaluation trace and suggestions for granting access",
2067
+ tip: "Use scan with filter='denied' to find exactly what permissions a SA is missing, then use the remediation flow to grant them.",
2068
+ },
2069
+ remediation_flow: {
2070
+ description: "The recommended way to grant permissions. Instead of manually creating roles and policies, use the remediation wizard — it finds the best option automatically.",
2071
+ steps: [
2072
+ "1. Identify the gap: scan_service_account_permissions(filter='denied') or simulate_service_account_permission",
2073
+ "2. Generate options: generate_remediation(serviceAccountId, resource, resourceCategory, actions) — returns multiple options ranked by effort (low/medium/high) with a recommended pick",
2074
+ "3. Preview changes: preview_remediation(same args + chosen option) — shows exactly what would be created/modified without making changes",
2075
+ "4. Apply: apply_remediation(same args + chosen option) — creates roles/policies/assignments and verifies access was granted",
2076
+ ],
2077
+ option_types: {
2078
+ role_assignment: "Assign an existing role that already covers the needed permissions (effort: low)",
2079
+ group_assignment: "Add the SA to a group that has the right role (effort: low)",
2080
+ create_new: "Create a minimal new policy + permission + role for exactly the requested access (effort: medium-high)",
2081
+ },
2082
+ tip: "Always prefer the 'recommended' option. If multiple options exist, role_assignment is usually lowest effort. The response includes sideEffects showing what additional access the SA would gain.",
2083
+ },
2084
+ secret_management: {
2085
+ creation: "clientSecret shown ONCE at creation — store securely immediately",
2086
+ rotation: "rotate_service_account_secret — old secret immediately invalidated, new one returned (shown once)",
2087
+ revocation: "revoke_service_account — permanent, cannot be undone, all tokens invalidated",
2088
+ },
2089
+ tools: {
2090
+ lifecycle: {
2091
+ list_service_accounts: "List all SAs in the workspace",
2092
+ get_service_account: "Get SA details (no secret)",
2093
+ create_service_account: "Create SA — returns clientId + clientSecret",
2094
+ update_service_account_name: "Update SA display name",
2095
+ update_service_account_description: "Update SA description",
2096
+ delete_service_account: "Permanently delete SA",
2097
+ rotate_service_account_secret: "Generate new secret, invalidate old one",
2098
+ revoke_service_account: "Permanently disable SA",
2099
+ generate_dev_token: "Short-lived token for testing",
2100
+ },
2101
+ permissions: {
2102
+ scan_service_account_permissions: "Full access matrix audit",
2103
+ simulate_service_account_permission: "Test specific resource+action check",
2104
+ generate_remediation: "Generate options to grant missing access (role assignment, group assignment, or new policy)",
2105
+ preview_remediation: "Preview what changes an option would make (dry run)",
2106
+ apply_remediation: "Apply a remediation option to actually grant access (creates roles/policies as needed)",
2107
+ },
2108
+ roles: {
2109
+ list_roles: "List all roles",
2110
+ get_role: "Get role details with permissions",
2111
+ create_role: "Create role with permissions",
2112
+ update_role: "Update role name/description/permissions",
2113
+ delete_role: "Delete role",
2114
+ list_service_account_roles: "List roles assigned to a SA",
2115
+ assign_role_to_service_account: "Assign role to SA",
2116
+ remove_role_from_service_account: "Remove role from SA",
2117
+ },
2118
+ groups: {
2119
+ list_groups: "List all groups",
2120
+ get_group: "Get group details",
2121
+ create_group: "Create group",
2122
+ update_group: "Update group name/description",
2123
+ delete_group: "Delete group",
2124
+ list_service_account_groups: "List groups a SA belongs to",
2125
+ add_service_account_to_group: "Add SA to group",
2126
+ remove_service_account_from_group: "Remove SA from group",
2127
+ },
2128
+ },
2129
+ publishable_keys: {
2130
+ description: "Frontend-safe API keys for browser/client-side apps. Scoped to specific collections, actions, and triggers.",
2131
+ key_format: "pk_live_<hex> — safe to embed in client-side code, scoped and rate-limited",
2132
+ scope_format: {
2133
+ description: "Scopes control what the key can access. Format: resource:action:target",
2134
+ examples: [
2135
+ "records:list:products — list products collection",
2136
+ "records:retrieve:* — read any collection (wildcard OK for reads)",
2137
+ "records:create:orders — create orders (write actions require explicit slug)",
2138
+ "triggers:execute:send-email — invoke a specific HTTP trigger",
2139
+ "files:retrieve — read files (two-part scope, no target)",
2140
+ "collections:list — list collection schemas",
2141
+ ],
2142
+ rules: [
2143
+ "Write actions (create, execute) require explicit collection/trigger slugs — no wildcards",
2144
+ "Read actions (retrieve, list) allow wildcard targets (*)",
2145
+ "Any records scope auto-grants collections:retrieve for that collection (schema access)",
2146
+ ],
2147
+ },
2148
+ tools: {
2149
+ list_publishable_keys: "List all publishable keys",
2150
+ get_publishable_key: "Get key details with scopes",
2151
+ create_publishable_key: "Create a new scoped key",
2152
+ update_publishable_key: "Update label or scopes",
2153
+ revoke_publishable_key: "Revoke a key (immediate, irreversible)",
2154
+ },
2155
+ },
2156
+ tips: [
2157
+ "LEAST PRIVILEGE: When granting permissions, only grant what the app actually needs. Use generate_remediation → prefer 'create_new' for exact access rather than broad role assignments.",
2158
+ "Use scan_service_account_permissions with filter='denied' to find missing permissions, then use the remediation flow to grant them",
2159
+ "Use simulate_service_account_permission to test before deploying — it shows the evaluation trace so you can debug policy decisions",
2160
+ "For frontend apps, use publishable keys (create_publishable_key) with scoped access — never expose service account credentials in client code",
2161
+ "For backend apps, use service accounts — they authenticate via OAuth2 client_credentials",
2162
+ "Group SAs by function (e.g., 'Backend Services', 'Analytics Pipeline') and assign roles to groups for easier management",
2163
+ "Always rotate secrets via rotate_service_account_secret rather than creating a new SA — preserves role/group assignments",
2164
+ "For development, use generate_dev_token instead of hardcoding client credentials in test scripts",
2165
+ "The clientSecret is only shown once — at creation and rotation. If lost, rotate to get a new one.",
2166
+ ],
2167
+ }, null, 2),
2168
+ },
2169
+ ],
2170
+ });
2171
+ }));
1678
2172
  }