@k2b/cloud 0.47.0 → 0.49.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.
- package/package.json +3 -3
- package/src/_internal/app-access-manager-sql.ts +10 -0
- package/src/_internal/app-access-resources.ts +2 -1
- package/src/_internal/app-capabilities.ts +73 -0
- package/src/_internal/app-event-catalog.ts +1 -1
- package/src/_internal/app-queries.ts +1 -1
- package/src/_internal/application-package.ts +2 -1
- package/src/_internal/bounded-json.ts +7 -1
- package/src/_internal/capabilities.ts +25 -2
- package/src/_internal/define-app.ts +33 -2
- package/src/_internal/heartbeat.ts +2 -0
- package/src/_internal/nats-connection.ts +10 -9
- package/src/_internal/process-sync.ts +6 -6
- package/src/_internal/public-surface.ts +76 -0
- package/src/_internal/purpose-name.ts +7 -0
- package/src/_internal/registry-validation.ts +1 -0
- package/src/_internal/registry.ts +49 -3
- package/src/_internal/workload-credential.ts +2 -1
- package/src/ai/app-skill-store.ts +2 -1
- package/src/ai/app-skills.ts +2 -1
- package/src/ai/approvals.ts +2 -1
- package/src/ai/chat-tasks.ts +2 -1
- package/src/ai/credentials.ts +2 -1
- package/src/ai/dictation-runtime.ts +1 -1
- package/src/ai/dictations.ts +1 -1
- package/src/ai/files-store.ts +2 -1
- package/src/ai/inference-calls.ts +2 -1
- package/src/ai/map-store.ts +1 -1
- package/src/ai/map-tool.ts +1 -1
- package/src/ai/memories.ts +1 -1
- package/src/ai/memory-learning-runs.ts +1 -1
- package/src/ai/memory-learning.ts +1 -1
- package/src/ai/memory-workflow-evidence.ts +1 -1
- package/src/ai/message-queue.ts +2 -1
- package/src/ai/migrate.ts +1 -1
- package/src/ai/model-access-migrate.ts +1 -1
- package/src/ai/model-access.ts +1 -1
- package/src/ai/model-catalog.ts +1 -1
- package/src/ai/model-delete.ts +1 -1
- package/src/ai/model-usage.ts +2 -1
- package/src/ai/open-questions.ts +1 -1
- package/src/ai/openai-api-slots.ts +2 -1
- package/src/ai/prefs.ts +2 -1
- package/src/ai/projects.ts +2 -1
- package/src/ai/provider-connections.ts +1 -1
- package/src/ai/quota-limits.ts +2 -1
- package/src/ai/quota-provider.ts +2 -1
- package/src/ai/quota-report.ts +2 -1
- package/src/ai/quotas-migrate.ts +2 -1
- package/src/ai/quotas.ts +2 -1
- package/src/ai/request-headers.ts +2 -1
- package/src/ai/review-content.ts +2 -1
- package/src/ai/skill-search.ts +2 -1
- package/src/ai/skills.ts +2 -1
- package/src/ai/store.ts +2 -1
- package/src/ai/structured-runs.ts +2 -1
- package/src/ai/tool-audit.ts +2 -1
- package/src/ai/turn-timing.ts +1 -1
- package/src/ai/usage.ts +2 -1
- package/src/ai/web-provenance.ts +2 -1
- package/src/api/admin-ai-quotas.ts +1 -1
- package/src/api/admin-ai.ts +1 -1
- package/src/api/admin-app-domains.ts +62 -0
- package/src/api/admin-app-keys.ts +47 -3
- package/src/api/admin-core-settings.ts +1 -1
- package/src/api/admin-outgoing-mail.ts +1 -1
- package/src/api/app-bootstrap.ts +153 -0
- package/src/api/app-events.ts +1 -1
- package/src/api/auth.ts +1 -1
- package/src/api/authenticate-app-request.ts +129 -0
- package/src/api/capabilities.ts +87 -6
- package/src/api/index.ts +7 -0
- package/src/api/me.ts +1 -1
- package/src/api/search.bench.ts +1 -1
- package/src/app-pwa/service.ts +2 -1
- package/src/app-pwa/transaction.ts +4 -2
- package/src/capabilities/claims.ts +2 -1
- package/src/capabilities/executions.ts +8 -4
- package/src/capabilities/geo-client.ts +1 -1
- package/src/capabilities/migrate.ts +3 -1
- package/src/capabilities/resource-readers.ts +2 -2
- package/src/capabilities/response.ts +2 -1
- package/src/capabilities/server-invoke.ts +73 -20
- package/src/capabilities/server.ts +2 -1
- package/src/cli/admin/app-domains.ts +140 -0
- package/src/cli/admin/app-keys.ts +25 -3
- package/src/cli/admin/index.ts +107 -88
- package/src/config/env.ts +10 -4
- package/src/contracts/app-domains.ts +91 -0
- package/src/contracts/app-status.ts +104 -0
- package/src/contracts/capabilities.ts +37 -2
- package/src/contracts/index.ts +1 -0
- package/src/contracts/outgoing-mail.ts +1 -1
- package/src/contracts/registry.ts +2 -0
- package/src/events/app-event-delivery.ts +1 -1
- package/src/events/live-delivery.ts +2 -1
- package/src/index.ts +1 -0
- package/src/runtime/bootstrap-client.ts +195 -0
- package/src/runtime/bootstrap-protocol.ts +67 -0
- package/src/runtime/clients.ts +6 -2
- package/src/runtime/connections.ts +9 -0
- package/src/server/app-key.ts +14 -0
- package/src/server/index.ts +2 -0
- package/src/server/ratelimit.ts +1 -1
- package/src/server/services/access.ts +2 -1
- package/src/services/account-category-policy.ts +1 -1
- package/src/services/account-lifecycle/audit.ts +1 -1
- package/src/services/account-lifecycle/index.ts +1 -1
- package/src/services/account-lifecycle/ipa-backfill.ts +2 -1
- package/src/services/accounts/app.ts +1 -1
- package/src/services/accounts/avatar.ts +1 -1
- package/src/services/accounts/base-group.ts +1 -1
- package/src/services/accounts/directory-shares.ts +1 -1
- package/src/services/accounts/duplicate-emails.ts +1 -1
- package/src/services/accounts/entities.ts +2 -1
- package/src/services/accounts/group-sql.ts +2 -1
- package/src/services/accounts/groups.ts +1 -1
- package/src/services/accounts/identities.ts +13 -11
- package/src/services/accounts/ipa-data.ts +1 -1
- package/src/services/accounts/local-groups.ts +2 -1
- package/src/services/accounts/machine-group-management.ts +2 -1
- package/src/services/accounts/posix.ts +13 -12
- package/src/services/accounts/request-policy.ts +2 -1
- package/src/services/accounts/switching.ts +1 -1
- package/src/services/accounts/users.ts +2 -1
- package/src/services/announcements/index.ts +1 -1
- package/src/services/app-access-management.ts +3 -9
- package/src/services/app-access.ts +2 -1
- package/src/services/app-admin-facts.ts +160 -0
- package/src/services/app-approval.ts +34 -30
- package/src/services/app-bootstrap.ts +217 -0
- package/src/services/app-details.ts +3 -2
- package/src/services/app-domain-check.ts +152 -0
- package/src/services/app-domains.ts +172 -0
- package/src/services/app-events/accept.ts +5 -5
- package/src/services/app-events/admin.ts +15 -15
- package/src/services/app-events/declarations.ts +6 -3
- package/src/services/app-events/deliver.ts +6 -6
- package/src/services/app-events.ts +3 -2
- package/src/services/app-keys.ts +45 -11
- package/src/services/approvals/content.ts +1 -1
- package/src/services/approvals/index.ts +2 -1
- package/src/services/approvals/list.ts +1 -1
- package/src/services/approvals/migrate.ts +1 -1
- package/src/services/audit/index.ts +2 -1
- package/src/services/auth-flows/ipa.ts +1 -1
- package/src/services/auth-flows/magic-link.ts +1 -1
- package/src/services/auth-flows/password-reset.ts +1 -1
- package/src/services/auth-flows/proxy-return.ts +1 -1
- package/src/services/downstream-budget.ts +2 -2
- package/src/services/geo/admin.ts +13 -12
- package/src/services/geo/identity.ts +46 -0
- package/src/services/geo/ledger.ts +118 -106
- package/src/services/geo/migrate.ts +2 -1
- package/src/services/geo/route-chain.ts +3 -3
- package/src/services/geo/service.ts +4 -24
- package/src/services/geoip/index.ts +5 -5
- package/src/services/geoip/store.ts +24 -21
- package/src/services/help/index.ts +2 -1
- package/src/services/help/store.ts +2 -1
- package/src/services/identity/app-request.ts +44 -0
- package/src/services/identity/constants.ts +1 -0
- package/src/services/identity/index.ts +1 -0
- package/src/services/identity/invocation-actor.ts +2 -1
- package/src/services/identity/invocation-token.ts +112 -0
- package/src/services/identity/key-ring.ts +2 -2
- package/src/services/identity/resource-readers.ts +1 -1
- package/src/services/identity/routes.ts +11 -3
- package/src/services/identity/runtime-config.ts +14 -3
- package/src/services/identity/workload-auth.ts +2 -0
- package/src/services/identity/workload-runtime.ts +3 -3
- package/src/services/index.ts +4 -0
- package/src/services/ipa/groups.ts +1 -1
- package/src/services/ipa/profile.ts +1 -1
- package/src/services/ipa/search.ts +1 -1
- package/src/services/ipa/sync.ts +1 -1
- package/src/services/ipa/users.ts +1 -1
- package/src/services/known-browsers.ts +2 -1
- package/src/services/legal-consent.ts +1 -1
- package/src/services/links/index.ts +16 -0
- package/src/services/links/runtime.ts +44 -0
- package/src/services/links/store.ts +105 -0
- package/src/services/logging/index.ts +13 -8
- package/src/services/logging/redaction.ts +6 -1
- package/src/services/logging/trace.ts +9 -9
- package/src/services/logging/web-vitals.ts +1 -1
- package/src/services/mandates/index.ts +2 -1
- package/src/services/notifications/batches.ts +1 -1
- package/src/services/notifications/browser.ts +1 -1
- package/src/services/notifications/catalog.ts +1 -1
- package/src/services/notifications/dispatcher.ts +1 -1
- package/src/services/notifications/index.ts +1 -1
- package/src/services/notifications/observability.ts +2 -1
- package/src/services/notifications/platform.ts +1 -1
- package/src/services/notifications/quiet.ts +1 -1
- package/src/services/notifications/user.ts +1 -1
- package/src/services/oauth-tokens.ts +1 -1
- package/src/services/outbox.ts +15 -14
- package/src/services/outgoing-mail/admin.ts +1 -1
- package/src/services/outgoing-mail/dispatcher.ts +1 -1
- package/src/services/outgoing-mail/drain.ts +2 -1
- package/src/services/outgoing-mail/enqueue.ts +2 -1
- package/src/services/outgoing-mail/messages.ts +2 -1
- package/src/services/outgoing-mail/retention.ts +1 -1
- package/src/services/outgoing-mail/send.ts +1 -1
- package/src/services/outgoing-mail/store.ts +2 -1
- package/src/services/panel-context-consent.ts +32 -28
- package/src/services/providers/local/auth.ts +1 -1
- package/src/services/providers/local/users.ts +1 -1
- package/src/services/pwa-devices.ts +2 -1
- package/src/services/rail-preferences.ts +16 -12
- package/src/services/rail-shortcuts.ts +12 -10
- package/src/services/request-cache-redis.ts +3 -3
- package/src/services/service-account-credentials.ts +2 -1
- package/src/services/service-accounts.ts +2 -1
- package/src/services/session/index.ts +2 -1
- package/src/services/session/user.ts +2 -1
- package/src/services/settings/core-settings.ts +20 -0
- package/src/services/settings/crypto.ts +2 -2
- package/src/services/settings/index.ts +1 -1
- package/src/services/settings/store.ts +6 -1
- package/src/services/settings/versions.ts +2 -1
- package/src/services/weather/forecast.ts +2 -1
- package/src/services/weather/locations.ts +1 -1
- package/src/services/weather/migrate.ts +2 -1
- package/src/services/weather/short-id.ts +2 -1
- package/src/services/webauthn.ts +1 -1
- package/src/ssr/admin-navigation.ts +0 -1
- package/src/ssr/compact-shell.browser-harness.ts +2 -0
- package/src/ssr/platform-messages.ts +0 -2
- package/src/start.ts +35 -14
- package/src/workflows/ai/store.ts +2 -1
- package/src/workflows/store/budget.ts +2 -1
- package/src/workflows/store/definitions.ts +2 -1
- package/src/workflows/store/events.ts +2 -1
- package/src/workflows/store/observability.ts +2 -1
- package/src/workflows/store/retention.ts +2 -1
- package/src/workflows/store/runs.ts +2 -1
- package/src/workflows/store/transaction.ts +2 -1
- package/src/workflows/store/worker.ts +2 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { isPromise } from "node:util/types";
|
|
2
2
|
import type { ReservedSQL, SQL, TransactionSQL } from "bun";
|
|
3
|
-
import
|
|
3
|
+
import { createSql } from "../runtime/clients";
|
|
4
4
|
import { REQUEST_IDLE_TIMEOUT_SECONDS } from "../server/request-timeout";
|
|
5
5
|
import { boundTransaction } from "./authority";
|
|
6
6
|
import { AppPwaError } from "./contracts";
|
|
@@ -22,8 +22,9 @@ const cancellationPool = (db: SQL) => {
|
|
|
22
22
|
const connection: Record<string, string | number> = {};
|
|
23
23
|
for (let index = 0; index + 1 < parts.length; index += 2) connection[parts[index]!] = parts[index + 1]!;
|
|
24
24
|
connection.statement_timeout = requestBudget;
|
|
25
|
-
return
|
|
25
|
+
return createSql({
|
|
26
26
|
...options,
|
|
27
|
+
adapter: "postgres",
|
|
27
28
|
// Bun adds SNI to normalized prefer options. Passing that generated object
|
|
28
29
|
// back as explicit TLS upgrades prefer to require. Recreate its default SNI
|
|
29
30
|
// from hostname instead; explicit verification options remain unchanged.
|
|
@@ -35,6 +36,7 @@ const cancellationPool = (db: SQL) => {
|
|
|
35
36
|
maxLifetime: REQUEST_IDLE_TIMEOUT_SECONDS,
|
|
36
37
|
onconnect: undefined,
|
|
37
38
|
onclose: undefined,
|
|
39
|
+
url: `postgres://localhost?sslmode=${modes[sslMode]}`,
|
|
38
40
|
});
|
|
39
41
|
};
|
|
40
42
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { sql } from "bun";
|
|
2
1
|
import { createCapabilityDeadline } from "../_internal/capability-timeout";
|
|
3
2
|
import type { CapabilityOrigin } from "../contracts/capabilities";
|
|
3
|
+
import { sql } from "../runtime/clients";
|
|
4
4
|
import { REQUEST_IDLE_TIMEOUT_SECONDS } from "../server/request-timeout";
|
|
5
5
|
import { pruneApprovalRequests } from "../services/approvals";
|
|
6
6
|
import { logger } from "../services/logging";
|
|
@@ -65,6 +65,8 @@ export type CapabilityExecutionInput = {
|
|
|
65
65
|
destructive: boolean;
|
|
66
66
|
actorKind?: "user" | "service_account" | "app" | null;
|
|
67
67
|
actorId?: string | null;
|
|
68
|
+
actorAppId?: string | null;
|
|
69
|
+
actorKeyId?: string | null;
|
|
68
70
|
userId?: string | null;
|
|
69
71
|
/** Recorded only when the access subject differs from the acting principal. */
|
|
70
72
|
accessSubject?: { type: "user" | "service_account"; id: string } | null;
|
|
@@ -92,6 +94,8 @@ export type CapabilityExecution = {
|
|
|
92
94
|
destructive: boolean;
|
|
93
95
|
actorKind: "user" | "service_account" | "app" | null;
|
|
94
96
|
actorId: string | null;
|
|
97
|
+
actorAppId?: string | null;
|
|
98
|
+
actorKeyId?: string | null;
|
|
95
99
|
userId: string | null;
|
|
96
100
|
accessSubjectType: "user" | "service_account" | null;
|
|
97
101
|
accessSubjectId: string | null;
|
|
@@ -122,12 +126,12 @@ export const recordCapabilityExecution = async (input: CapabilityExecutionInput)
|
|
|
122
126
|
const [row] = await db<{ id: string }[]>`
|
|
123
127
|
INSERT INTO capabilities.executions (
|
|
124
128
|
request_id, invocation_id, parent_invocation_id, via_app_id, origin, app_id, capability, kind, destructive,
|
|
125
|
-
actor_kind, actor_id, user_id, access_subject_type, access_subject_id,
|
|
129
|
+
actor_kind, actor_id, actor_app_id, actor_key_id, user_id, access_subject_type, access_subject_id,
|
|
126
130
|
status, error_code, input_meta, output_meta, idempotency_key, replayed,
|
|
127
131
|
started_at, completed_at, duration_ms
|
|
128
132
|
) VALUES (
|
|
129
133
|
${input.requestId}, ${input.invocationId ?? null}, ${input.parentInvocationId ?? null}, ${input.viaAppId ?? null}, ${input.origin}, ${input.appId}, ${input.capability}, ${input.kind}, ${input.destructive},
|
|
130
|
-
${input.actorKind ?? null}, ${input.actorId ?? null}, ${input.userId ?? null},
|
|
134
|
+
${input.actorKind ?? null}, ${input.actorId ?? null}, ${input.actorAppId ?? null}, ${input.actorKeyId ?? null}::uuid, ${input.userId ?? null},
|
|
131
135
|
${input.accessSubject?.type ?? null}, ${input.accessSubject?.id ?? null},
|
|
132
136
|
${input.status}, ${input.errorCode ?? null},
|
|
133
137
|
${input.inputMeta ? JSON.stringify(input.inputMeta) : null}::text::jsonb,
|
|
@@ -177,7 +181,7 @@ const columns = () => sql`
|
|
|
177
181
|
execution.id::text AS id, execution.request_id AS "requestId",
|
|
178
182
|
execution.invocation_id::text AS "invocationId", execution.parent_invocation_id::text AS "parentInvocationId", execution.via_app_id AS "viaAppId", execution.origin, execution.app_id AS "appId",
|
|
179
183
|
execution.capability, execution.kind, execution.destructive,
|
|
180
|
-
execution.actor_kind AS "actorKind", execution.actor_id::text AS "actorId", execution.user_id::text AS "userId",
|
|
184
|
+
execution.actor_app_id AS "actorAppId", execution.actor_key_id::text AS "actorKeyId", execution.actor_kind AS "actorKind", execution.actor_id::text AS "actorId", execution.user_id::text AS "userId",
|
|
181
185
|
execution.access_subject_type AS "accessSubjectType", execution.access_subject_id::text AS "accessSubjectId",
|
|
182
186
|
execution.status, execution.error_code AS "errorCode", execution.input_meta AS "inputMeta",
|
|
183
187
|
execution.output_meta AS "outputMeta", execution.idempotency_key AS "idempotencyKey", execution.replayed,
|
|
@@ -4,7 +4,7 @@ import { geoMapContract } from "../contracts/geo-map";
|
|
|
4
4
|
import { geoRouteContract } from "../contracts/geo-route";
|
|
5
5
|
import { type CapabilityCaller, invokeCapabilityWithDataSchema } from "./server";
|
|
6
6
|
|
|
7
|
-
/** Calls Core using the caller's interactive identity, invocation transport, or durable mandate. */
|
|
7
|
+
/** Calls Core using the caller's interactive identity, invocation transport, app authority, or durable mandate. */
|
|
8
8
|
export const geoClient = {
|
|
9
9
|
mapStyle: (input: z.input<typeof geoMapContract.style.input>, caller: CapabilityCaller) =>
|
|
10
10
|
invokeCapabilityWithDataSchema(
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { sql } from "
|
|
1
|
+
import { sql } from "../runtime/clients";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Platform-owned capability execution history.
|
|
@@ -58,6 +58,8 @@ export const migrateCloudCapabilities = async (): Promise<void> => {
|
|
|
58
58
|
await sql`ALTER TABLE capabilities.executions ADD COLUMN IF NOT EXISTS invocation_id UUID`.simple();
|
|
59
59
|
await sql`ALTER TABLE capabilities.executions ADD COLUMN IF NOT EXISTS parent_invocation_id UUID`.simple();
|
|
60
60
|
await sql`ALTER TABLE capabilities.executions ADD COLUMN IF NOT EXISTS via_app_id TEXT`.simple();
|
|
61
|
+
await sql`ALTER TABLE capabilities.executions ADD COLUMN IF NOT EXISTS actor_app_id TEXT`.simple();
|
|
62
|
+
await sql`ALTER TABLE capabilities.executions ADD COLUMN IF NOT EXISTS actor_key_id UUID`.simple();
|
|
61
63
|
|
|
62
64
|
await sql`DO $$ BEGIN
|
|
63
65
|
IF EXISTS (SELECT 1 FROM pg_constraint WHERE conrelid = 'capabilities.executions'::regclass
|
|
@@ -33,7 +33,7 @@ export const readResourcePrincipals = async (options: {
|
|
|
33
33
|
const ref = CloudResourceRefSchema.parse(options.ref);
|
|
34
34
|
// Core issues the credential automatically; an explicit CLOUD_APP_CREDENTIAL override wins.
|
|
35
35
|
const credential = getAppWorkloadCredential();
|
|
36
|
-
if (!credential) return { state: "not_configured", reason: "no_credential" };
|
|
36
|
+
if (!env.CLOUD_APP_KEY && !credential) return { state: "not_configured", reason: "no_credential" };
|
|
37
37
|
if (!env.CLOUD_CORE_INTERNAL_ORIGIN) return { state: "not_configured", reason: "no_internal_origin" };
|
|
38
38
|
const callerAppId = getProcessApplicationId();
|
|
39
39
|
if (!callerAppId) return { state: "not_configured", reason: "no_app_identity" };
|
|
@@ -54,7 +54,7 @@ export const readResourcePrincipals = async (options: {
|
|
|
54
54
|
ResourceReadableDataSchema,
|
|
55
55
|
{
|
|
56
56
|
workload: { appId: callerAppId },
|
|
57
|
-
authorization: `Bearer ${credential}
|
|
57
|
+
...(credential ? { authorization: `Bearer ${credential}` } : {}),
|
|
58
58
|
signal: options.signal,
|
|
59
59
|
locale: options.locale,
|
|
60
60
|
requestId: options.requestId,
|
|
@@ -8,8 +8,9 @@ export const readCapabilityResponse = async <T>(
|
|
|
8
8
|
schema: z.ZodType<T>,
|
|
9
9
|
maxBytes = CAPABILITY_MAX_RESULT_BYTES,
|
|
10
10
|
signal?: AbortSignal,
|
|
11
|
+
consumeBytes?: (bytes: number) => void,
|
|
11
12
|
): Promise<CapabilityResultState<T>> => {
|
|
12
|
-
const parsedBody = await readBoundedJson(response, maxBytes, signal);
|
|
13
|
+
const parsedBody = await readBoundedJson(response, maxBytes, signal, consumeBytes);
|
|
13
14
|
if (!parsedBody.ok && parsedBody.reason === "too_large") {
|
|
14
15
|
return {
|
|
15
16
|
ok: false,
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { getProcessApplicationId } from "../_internal/process-identity";
|
|
2
3
|
import { refreshAppWorkloadCredential } from "../_internal/workload-credential";
|
|
3
4
|
import { dispatchCapability, loadCapabilityCatalogPage } from "../api/capabilities";
|
|
5
|
+
import { env } from "../config/env";
|
|
4
6
|
import {
|
|
5
7
|
CAPABILITY_FRAMEWORK_ERROR_CODES,
|
|
6
8
|
CAPABILITY_MAX_CATALOG_BYTES,
|
|
9
|
+
CAPABILITY_MAX_RESULT_BYTES,
|
|
7
10
|
CapabilityCatalogSchema,
|
|
8
11
|
type CapabilityOrigin,
|
|
9
12
|
capabilityResultSchema,
|
|
10
13
|
} from "../contracts/capabilities";
|
|
11
14
|
import type { RequestAuthority } from "../server";
|
|
15
|
+
import { sendAppRequest } from "../services/identity/app-request";
|
|
12
16
|
import { coreOrigin } from "../services/identity/core-origin";
|
|
13
17
|
import { capabilityMessages } from "../shared/capability-messages";
|
|
14
18
|
import { LOCALE_HEADER } from "../shared/locale";
|
|
@@ -36,12 +40,24 @@ export type CapabilityCaller = {
|
|
|
36
40
|
/** BCP 47 locale of the originating request, forwarded as invocation metadata. */
|
|
37
41
|
locale?: string | null;
|
|
38
42
|
signal?: AbortSignal;
|
|
39
|
-
/**
|
|
43
|
+
/** Request-local aggregate response budget; called before result validation for every received chunk. */
|
|
44
|
+
consumeResponseBytes?: (bytes: number) => void;
|
|
45
|
+
/** App-only query authority. The platform authenticates each transmission to Core. */
|
|
40
46
|
workload?: { appId: string };
|
|
41
|
-
/**
|
|
47
|
+
/** App authority authenticated only by the configured Ed25519 app key. */
|
|
48
|
+
app?: { appId: string };
|
|
49
|
+
/** Durable background authority. The platform authenticates each transmission to Core. */
|
|
42
50
|
mandate?: { id: string; revision: number; callingAppId: string };
|
|
43
51
|
};
|
|
44
52
|
|
|
53
|
+
/** Call as the started application, without a person, service account token, or mandate. */
|
|
54
|
+
export const appCaller = (options: Pick<CapabilityCaller, "locale" | "requestId" | "signal"> = {}): CapabilityCaller => {
|
|
55
|
+
const appId = getProcessApplicationId();
|
|
56
|
+
if (!appId) throw new Error("Start the application before creating its capability caller");
|
|
57
|
+
if (!env.CLOUD_APP_KEY) throw new Error("App capability calls require CLOUD_APP_KEY");
|
|
58
|
+
return { ...options, app: { appId }, origin: "app" };
|
|
59
|
+
};
|
|
60
|
+
|
|
45
61
|
export { coreOrigin } from "../services/identity/core-origin";
|
|
46
62
|
|
|
47
63
|
export const callerRequest = async (
|
|
@@ -52,7 +68,7 @@ export const callerRequest = async (
|
|
|
52
68
|
): Promise<Request> => {
|
|
53
69
|
const headers = new Headers();
|
|
54
70
|
if (caller.authorization) headers.set("authorization", caller.authorization);
|
|
55
|
-
if (!caller.mandate && !caller.workload) {
|
|
71
|
+
if (!caller.mandate && !caller.workload && !caller.app) {
|
|
56
72
|
if (caller.cookie) headers.set("cookie", caller.cookie);
|
|
57
73
|
if (caller.requestId) headers.set("x-request-id", caller.requestId);
|
|
58
74
|
if (caller.traceparent) headers.set("traceparent", caller.traceparent);
|
|
@@ -60,14 +76,17 @@ export const callerRequest = async (
|
|
|
60
76
|
if (caller.locale) headers.set(LOCALE_HEADER, caller.locale);
|
|
61
77
|
if (idempotencyKey) headers.set("idempotency-key", idempotencyKey);
|
|
62
78
|
} else {
|
|
63
|
-
headers.set("x-cloud-app-id", caller.mandate?.callingAppId ?? caller.workload!.appId);
|
|
79
|
+
headers.set("x-cloud-app-id", caller.app?.appId ?? caller.mandate?.callingAppId ?? caller.workload!.appId);
|
|
64
80
|
}
|
|
65
81
|
const signal = combineCapabilitySignals(caller.signal, invocationSignal);
|
|
66
|
-
return new Request(
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
82
|
+
return new Request(
|
|
83
|
+
new URL(path, await coreOrigin(caller.app !== undefined || caller.mandate !== undefined || caller.workload !== undefined)),
|
|
84
|
+
{
|
|
85
|
+
method: "POST",
|
|
86
|
+
headers,
|
|
87
|
+
signal,
|
|
88
|
+
},
|
|
89
|
+
);
|
|
71
90
|
};
|
|
72
91
|
|
|
73
92
|
export const unavailable = (
|
|
@@ -103,16 +122,23 @@ const invokeCapabilityWithResultSchema = async <TDataSchema extends z.ZodType, T
|
|
|
103
122
|
caller: CapabilityCaller,
|
|
104
123
|
): Promise<CapabilityClientResult<z.output<TDataSchema>>> => {
|
|
105
124
|
try {
|
|
125
|
+
if (
|
|
126
|
+
caller.app &&
|
|
127
|
+
(!env.CLOUD_APP_KEY || caller.authority || caller.authorization || caller.cookie || caller.workload || caller.mandate)
|
|
128
|
+
)
|
|
129
|
+
throw new Error("App calls require one app-key authority source");
|
|
106
130
|
if (caller.workload && (caller.mandate || caller.cookie || invocation.kind !== "query"))
|
|
107
131
|
throw new Error("Workload authority permits only queries without a mandate or cookie");
|
|
108
132
|
const path =
|
|
109
|
-
caller.mandate || caller.workload
|
|
133
|
+
caller.app || caller.mandate || caller.workload
|
|
110
134
|
? "/api/_internal/identity/v1/invoke"
|
|
111
135
|
: `/api/capabilities/v1/${invocation.kind === "query" ? "queries" : "actions"}/${encodeURIComponent(invocation.appId)}/${encodeURIComponent(invocation.capabilityId)}`;
|
|
112
136
|
const request = await callerRequest(caller, path, invocation.idempotencyKey, invocation.signal);
|
|
113
137
|
if (caller.authority) {
|
|
114
|
-
if (caller.authorization || caller.cookie || caller.mandate || caller.workload || caller.transport)
|
|
138
|
+
if (caller.authorization || caller.cookie || caller.app || caller.mandate || caller.workload || caller.transport)
|
|
115
139
|
throw new Error("Use exactly one capability authority source");
|
|
140
|
+
let charged = false;
|
|
141
|
+
const charge = caller.consumeResponseBytes;
|
|
116
142
|
const outcome = await dispatchCapability({
|
|
117
143
|
request,
|
|
118
144
|
kind: invocation.kind === "query" ? "queries" : "actions",
|
|
@@ -122,13 +148,29 @@ const invokeCapabilityWithResultSchema = async <TDataSchema extends z.ZodType, T
|
|
|
122
148
|
authority: caller.authority,
|
|
123
149
|
locale: caller.locale ?? undefined,
|
|
124
150
|
origin: caller.origin ?? "app",
|
|
151
|
+
...(charge
|
|
152
|
+
? {
|
|
153
|
+
consumeResponseBytes: (bytes: number) => {
|
|
154
|
+
charged = true;
|
|
155
|
+
charge(bytes);
|
|
156
|
+
},
|
|
157
|
+
}
|
|
158
|
+
: {}),
|
|
125
159
|
});
|
|
126
|
-
return readCapabilityResponse(
|
|
160
|
+
return readCapabilityResponse(
|
|
161
|
+
outcome,
|
|
162
|
+
capabilityResultSchema(dataSchema, { consumer: true }),
|
|
163
|
+
CAPABILITY_MAX_RESULT_BYTES,
|
|
164
|
+
// Only a budgeted caller cancels reading a response that already arrived; others keep the received result.
|
|
165
|
+
charge ? combineCapabilitySignals(caller.signal, invocation.signal) : undefined,
|
|
166
|
+
charged ? undefined : charge,
|
|
167
|
+
);
|
|
127
168
|
}
|
|
128
169
|
const brokerRequest = new Request(request, {
|
|
129
170
|
body: JSON.stringify(
|
|
130
|
-
caller.mandate || caller.workload
|
|
171
|
+
caller.app || caller.mandate || caller.workload
|
|
131
172
|
? {
|
|
173
|
+
...(caller.app ? { appCaller: true } : {}),
|
|
132
174
|
kind: invocation.kind,
|
|
133
175
|
targetApp: invocation.appId,
|
|
134
176
|
capabilityId: invocation.capabilityId,
|
|
@@ -149,13 +191,15 @@ const invokeCapabilityWithResultSchema = async <TDataSchema extends z.ZodType, T
|
|
|
149
191
|
: { input: invocation.input },
|
|
150
192
|
),
|
|
151
193
|
});
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
194
|
+
const workloadAppId = caller.app?.appId ?? caller.mandate?.callingAppId ?? caller.workload?.appId;
|
|
195
|
+
const send = (request: Request) =>
|
|
196
|
+
workloadAppId ? sendAppRequest(request, { appId: workloadAppId, transport: caller.transport }) : (caller.transport ?? fetch)(request);
|
|
197
|
+
// Only Core's pre-dispatch legacy rejection is safe to refresh and retry.
|
|
198
|
+
// A configured app key is never downgraded to a reusable credential.
|
|
155
199
|
const retryRequest = workloadAppId ? brokerRequest.clone() : undefined;
|
|
156
200
|
let response = await send(brokerRequest);
|
|
157
201
|
const credentialRejected = (reply: Response) => reply.status === 401 && reply.headers.get("x-cloud-workload-auth") === "required";
|
|
158
|
-
if (workloadAppId && retryRequest && credentialRejected(response)) {
|
|
202
|
+
if (!caller.app && workloadAppId && retryRequest && !env.CLOUD_APP_KEY && credentialRejected(response)) {
|
|
159
203
|
await response.body?.cancel();
|
|
160
204
|
let token: string | undefined;
|
|
161
205
|
try {
|
|
@@ -170,7 +214,13 @@ const invokeCapabilityWithResultSchema = async <TDataSchema extends z.ZodType, T
|
|
|
170
214
|
return unavailable(undefined, undefined, caller.locale ?? undefined);
|
|
171
215
|
}
|
|
172
216
|
}
|
|
173
|
-
return readCapabilityResponse(
|
|
217
|
+
return readCapabilityResponse(
|
|
218
|
+
response,
|
|
219
|
+
capabilityResultSchema(dataSchema, { consumer: true }),
|
|
220
|
+
CAPABILITY_MAX_RESULT_BYTES,
|
|
221
|
+
caller.consumeResponseBytes ? combineCapabilitySignals(caller.signal, invocation.signal) : undefined,
|
|
222
|
+
caller.consumeResponseBytes,
|
|
223
|
+
);
|
|
174
224
|
} catch (cause) {
|
|
175
225
|
return unavailable(cause, invocation, caller.locale ?? undefined);
|
|
176
226
|
}
|
|
@@ -202,14 +252,17 @@ export const listCapabilityCatalog = async (
|
|
|
202
252
|
if (options.caller) {
|
|
203
253
|
const caller = options.caller;
|
|
204
254
|
if (options.authority) throw new Error("Use one catalog authority source");
|
|
205
|
-
if (
|
|
255
|
+
if (
|
|
256
|
+
caller.authority &&
|
|
257
|
+
(caller.authorization || caller.cookie || caller.app || caller.mandate || caller.workload || caller.transport)
|
|
258
|
+
)
|
|
206
259
|
throw new Error("Use exactly one capability authority source");
|
|
207
260
|
if (caller.authority)
|
|
208
261
|
return {
|
|
209
262
|
ok: true,
|
|
210
263
|
data: await loadCapabilityCatalogPage({ cursor: options.cursor, limit }, {}, caller.locale ?? options.locale, caller.authority),
|
|
211
264
|
};
|
|
212
|
-
if (caller.mandate || caller.workload) throw new Error("Calendar discovery requires request credentials");
|
|
265
|
+
if (caller.app || caller.mandate || caller.workload) throw new Error("Calendar discovery requires request credentials");
|
|
213
266
|
const query = new URLSearchParams({ limit: String(limit) });
|
|
214
267
|
if (options.cursor) query.set("cursor", options.cursor);
|
|
215
268
|
const request = new Request(await callerRequest(caller, `/api/capabilities/v1/catalog?${query}`), { method: "GET" });
|
|
@@ -19,6 +19,7 @@ import type { CapabilityCatalogAppClientResult, CapabilityInvocation, Capability
|
|
|
19
19
|
|
|
20
20
|
export type { CapabilityActionWording } from "../_internal/capability-sentences";
|
|
21
21
|
export {
|
|
22
|
+
appCaller,
|
|
22
23
|
type CapabilityCaller,
|
|
23
24
|
invokeCapability,
|
|
24
25
|
invokeCapabilityWithDataSchema,
|
|
@@ -89,7 +90,7 @@ export const reviewCapabilityAction = async <TInput = unknown>(
|
|
|
89
90
|
caller: CapabilityCaller,
|
|
90
91
|
): Promise<CapabilityReviewClientResult> => {
|
|
91
92
|
try {
|
|
92
|
-
if (caller.workload) throw new Error("App query authority cannot review Actions");
|
|
93
|
+
if (caller.app || caller.workload) throw new Error("App query authority cannot review Actions");
|
|
93
94
|
if (caller.authority) {
|
|
94
95
|
if (caller.authorization || caller.cookie || caller.mandate || caller.transport)
|
|
95
96
|
throw new Error("Use exactly one capability authority source");
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { arg, type CloudCliContext, cliText, command, confirmFlag, flag, localizeCloudCliText, printStructured } from "../index";
|
|
3
|
+
import { apiGet, apiJson } from "./shared";
|
|
4
|
+
|
|
5
|
+
const step = z.object({ ok: z.boolean(), detail: z.string() });
|
|
6
|
+
const status = z.object({ checkedAt: z.string(), dns: step, certificate: step.nullable(), reachable: step.nullable() });
|
|
7
|
+
const domain = z.object({
|
|
8
|
+
host: z.string(),
|
|
9
|
+
redirectTo: z.string().nullable(),
|
|
10
|
+
createdAt: z.string(),
|
|
11
|
+
active: z.boolean(),
|
|
12
|
+
status: status.nullable(),
|
|
13
|
+
});
|
|
14
|
+
const path = (appId: string, host?: string) =>
|
|
15
|
+
`/api/admin/apps/${encodeURIComponent(appId)}/domains${host === undefined ? "" : `/${encodeURIComponent(host)}`}`;
|
|
16
|
+
const appArgs = { appId: arg.required({ valueLabel: "appId" }) };
|
|
17
|
+
const domainArgs = { ...appArgs, host: arg.required({ valueLabel: "host" }) };
|
|
18
|
+
const show = (ctx: CloudCliContext, raw: unknown, domains: z.infer<typeof domain>[]) => {
|
|
19
|
+
if (printStructured(ctx, raw)) return;
|
|
20
|
+
const checked = (result: z.infer<typeof step> | null | undefined) =>
|
|
21
|
+
result == null ? "-" : result.ok ? "ok" : cliText(ctx, { en: "failed", de: "fehlgeschlagen" });
|
|
22
|
+
ctx.table(
|
|
23
|
+
domains.map((item) => ({
|
|
24
|
+
host: item.host,
|
|
25
|
+
redirect: item.redirectTo ?? "-",
|
|
26
|
+
active: item.active,
|
|
27
|
+
dns: checked(item.status?.dns),
|
|
28
|
+
certificate: checked(item.status?.certificate),
|
|
29
|
+
reachable: checked(item.status?.reachable),
|
|
30
|
+
})),
|
|
31
|
+
[
|
|
32
|
+
{ key: "host", label: "Host" },
|
|
33
|
+
{ key: "redirect", label: cliText(ctx, { en: "Redirect", de: "Weiterleitung" }) },
|
|
34
|
+
{ key: "active", label: cliText(ctx, { en: "Active", de: "Aktiv" }) },
|
|
35
|
+
{ key: "dns", label: "DNS" },
|
|
36
|
+
{ key: "certificate", label: cliText(ctx, { en: "Certificate", de: "Zertifikat" }) },
|
|
37
|
+
{ key: "reachable", label: cliText(ctx, { en: "Reachable", de: "Erreichbar" }) },
|
|
38
|
+
],
|
|
39
|
+
);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const appDomainCommands = (locale?: string) => {
|
|
43
|
+
const t = (en: string, de: string) => localizeCloudCliText(locale, { en, de });
|
|
44
|
+
return [
|
|
45
|
+
command("apps domains ls", {
|
|
46
|
+
summary: t(
|
|
47
|
+
"List app domains, redirects and DNS/TLS/gateway status",
|
|
48
|
+
"App-Domains, Weiterleitungen und DNS/TLS/Gateway-Status auflisten",
|
|
49
|
+
),
|
|
50
|
+
args: appArgs,
|
|
51
|
+
async run({ ctx, args }) {
|
|
52
|
+
const result = z.object({ domains: z.array(domain) }).parse(await apiGet(ctx, path(args.appId)));
|
|
53
|
+
show(ctx, result, result.domains);
|
|
54
|
+
},
|
|
55
|
+
}),
|
|
56
|
+
command("apps domains add", {
|
|
57
|
+
summary: t(
|
|
58
|
+
"Assign a domain to a registered app and start its first check",
|
|
59
|
+
"Eine Domain einer bekannten App zuordnen und erstmals prüfen",
|
|
60
|
+
),
|
|
61
|
+
args: domainArgs,
|
|
62
|
+
flags: {
|
|
63
|
+
redirectTo: flag.string({
|
|
64
|
+
description: t("Another domain of this app without a redirect", "Eine andere Domain dieser App ohne Weiterleitung"),
|
|
65
|
+
}),
|
|
66
|
+
},
|
|
67
|
+
async run({ ctx, args, flags }) {
|
|
68
|
+
const result = domain.parse(
|
|
69
|
+
await apiJson(ctx, "POST", path(args.appId), { host: args.host, ...(flags.redirectTo ? { redirectTo: flags.redirectTo } : {}) }),
|
|
70
|
+
);
|
|
71
|
+
show(ctx, result, [result]);
|
|
72
|
+
},
|
|
73
|
+
}),
|
|
74
|
+
command("apps domains set", {
|
|
75
|
+
summary: t("Set or clear an app domain redirect", "Eine Domain-Weiterleitung setzen oder entfernen"),
|
|
76
|
+
args: domainArgs,
|
|
77
|
+
flags: {
|
|
78
|
+
redirectTo: flag.string({
|
|
79
|
+
description: t("Another domain of this app without a redirect", "Eine andere Domain dieser App ohne Weiterleitung"),
|
|
80
|
+
}),
|
|
81
|
+
noRedirect: flag.boolean({ description: t("Clear the redirect", "Die Weiterleitung entfernen") }),
|
|
82
|
+
},
|
|
83
|
+
async run({ ctx, args, flags }) {
|
|
84
|
+
if (Boolean(flags.redirectTo) === flags.noRedirect)
|
|
85
|
+
throw new Error(
|
|
86
|
+
cliText(ctx, {
|
|
87
|
+
en: "Use exactly one of --redirect-to or --no-redirect.",
|
|
88
|
+
de: "Nutze genau eine Option: --redirect-to oder --no-redirect.",
|
|
89
|
+
}),
|
|
90
|
+
);
|
|
91
|
+
const result = domain.parse(
|
|
92
|
+
await apiJson(ctx, "PATCH", path(args.appId, args.host), { redirectTo: flags.noRedirect ? null : flags.redirectTo }),
|
|
93
|
+
);
|
|
94
|
+
show(ctx, result, [result]);
|
|
95
|
+
},
|
|
96
|
+
}),
|
|
97
|
+
command("apps domains rm", {
|
|
98
|
+
summary: t("Remove an unreferenced app domain assignment", "Eine App-Domain ohne eingehende Weiterleitungen entfernen"),
|
|
99
|
+
args: domainArgs,
|
|
100
|
+
flags: { yes: confirmFlag(t("Remove this domain assignment", "Diese Domain-Zuordnung entfernen")) },
|
|
101
|
+
async run({ ctx, args, flags }) {
|
|
102
|
+
if (!flags.yes)
|
|
103
|
+
throw new Error(cliText(ctx, { en: "Removing a domain requires --yes.", de: "Das Entfernen einer Domain erfordert --yes." }));
|
|
104
|
+
const result = await apiJson(ctx, "DELETE", path(args.appId, args.host));
|
|
105
|
+
if (!printStructured(ctx, result)) ctx.print(cliText(ctx, { en: "Domain removed.", de: "Domain entfernt." }));
|
|
106
|
+
},
|
|
107
|
+
}),
|
|
108
|
+
command("apps domains check", {
|
|
109
|
+
summary: t(
|
|
110
|
+
"Check DNS, certificate and gateway reachability, at most once per 60 seconds",
|
|
111
|
+
"DNS, Zertifikat und Gateway-Erreichbarkeit höchstens einmal pro 60 Sekunden prüfen",
|
|
112
|
+
),
|
|
113
|
+
args: domainArgs,
|
|
114
|
+
async run({ ctx, args }) {
|
|
115
|
+
const result = z.object({ status: status.nullable() }).parse(await apiJson(ctx, "POST", `${path(args.appId, args.host)}/check`));
|
|
116
|
+
if (printStructured(ctx, result)) return;
|
|
117
|
+
if (!result.status) {
|
|
118
|
+
ctx.print(
|
|
119
|
+
cliText(ctx, {
|
|
120
|
+
en: "The first check is running. List the domains again shortly.",
|
|
121
|
+
de: "Die erste Prüfung läuft. Liste die Domains gleich erneut auf.",
|
|
122
|
+
}),
|
|
123
|
+
);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
ctx.table(
|
|
127
|
+
(["dns", "certificate", "reachable"] as const).map((key) => {
|
|
128
|
+
const value = result.status![key];
|
|
129
|
+
return { step: key, ok: value?.ok ?? "-", detail: value?.detail ?? "-" };
|
|
130
|
+
}),
|
|
131
|
+
[
|
|
132
|
+
{ key: "step", label: cliText(ctx, { en: "Step", de: "Schritt" }) },
|
|
133
|
+
{ key: "ok", label: "OK" },
|
|
134
|
+
{ key: "detail", label: "Detail" },
|
|
135
|
+
],
|
|
136
|
+
);
|
|
137
|
+
},
|
|
138
|
+
}),
|
|
139
|
+
];
|
|
140
|
+
};
|
|
@@ -78,7 +78,8 @@ export const enrollAppKey = async (input: {
|
|
|
78
78
|
format: AppKeyArtifactFormat;
|
|
79
79
|
uid?: number;
|
|
80
80
|
name?: string;
|
|
81
|
-
|
|
81
|
+
grants?: "oauth:issue"[];
|
|
82
|
+
register: (body: { publicKey: string; name?: string; grants?: "oauth:issue"[] }) => Promise<unknown>;
|
|
82
83
|
}) => {
|
|
83
84
|
validateAppKeyAppId(input.appId);
|
|
84
85
|
if (input.uid !== undefined && process.getuid?.() !== 0 && process.getuid?.() !== input.uid)
|
|
@@ -113,7 +114,11 @@ export const enrollAppKey = async (input: {
|
|
|
113
114
|
if ((await deriveAppPublicKey(state.seed)) !== state.publicKey) throw new Error("Enrollment public key does not match its seed");
|
|
114
115
|
await owner(provisional, input.uid);
|
|
115
116
|
const registered = registrationSchema.parse(
|
|
116
|
-
await input.register({
|
|
117
|
+
await input.register({
|
|
118
|
+
publicKey: state.publicKey,
|
|
119
|
+
...(input.name ? { name: input.name } : {}),
|
|
120
|
+
...(input.grants ? { grants: input.grants } : {}),
|
|
121
|
+
}),
|
|
117
122
|
);
|
|
118
123
|
if (registered.fingerprint !== (await appKeyFingerprint(state.publicKey)))
|
|
119
124
|
throw new Error("Core returned a different app-key fingerprint");
|
|
@@ -161,8 +166,11 @@ export const appKeyCommands = [
|
|
|
161
166
|
format: flag.enum(["file", "env", "compose", "k8s-secret"] as const, { default: "file" }),
|
|
162
167
|
uid: flag.int({ min: 0, description: "File owner UID; requires root or the current UID" }),
|
|
163
168
|
name: flag.string({ description: "Human-readable key name" }),
|
|
169
|
+
grant: flag.enum(["oauth:issue"] as const, { description: "Grant OAuth issuance to each new key; repeat on rotation" }),
|
|
170
|
+
noGrant: flag.boolean({ name: "no-grant", description: "Enroll new keys without grants (the default)" }),
|
|
164
171
|
},
|
|
165
172
|
async run({ ctx, args, flags }) {
|
|
173
|
+
if (flags.grant && flags.noGrant) throw new Error("Choose either --grant or --no-grant");
|
|
166
174
|
const apps = await resolveAppKeyApps(args.apps, flags.all, flags.apps);
|
|
167
175
|
const results = [];
|
|
168
176
|
for (const appId of apps)
|
|
@@ -174,6 +182,7 @@ export const appKeyCommands = [
|
|
|
174
182
|
format: flags.format ?? "file",
|
|
175
183
|
uid: flags.uid,
|
|
176
184
|
name: flags.name,
|
|
185
|
+
grants: flags.grant ? [flags.grant] : flags.noGrant ? [] : undefined,
|
|
177
186
|
register: (body) => apiJson(ctx, "POST", keyPath(appId), body),
|
|
178
187
|
}),
|
|
179
188
|
);
|
|
@@ -194,6 +203,14 @@ export const appKeyCommands = [
|
|
|
194
203
|
const result = z
|
|
195
204
|
.object({
|
|
196
205
|
installationId: z.uuid(),
|
|
206
|
+
lastRejectedStart: z
|
|
207
|
+
.object({
|
|
208
|
+
at: z.iso.datetime(),
|
|
209
|
+
reason: z.enum(["revoked", "expired", "disabled", "connections_not_permitted"]),
|
|
210
|
+
keyId: z.uuid().nullable(),
|
|
211
|
+
})
|
|
212
|
+
.nullable()
|
|
213
|
+
.optional(),
|
|
197
214
|
items: z.array(
|
|
198
215
|
z
|
|
199
216
|
.object({ keyId: z.string(), name: z.string(), fingerprint: z.string(), status: z.string(), accountStatus: z.string() })
|
|
@@ -202,7 +219,11 @@ export const appKeyCommands = [
|
|
|
202
219
|
})
|
|
203
220
|
.passthrough()
|
|
204
221
|
.parse(await apiGet(ctx, `${keyPath(args.app)}?page=${flags.page}&perPage=${flags.perPage}`));
|
|
205
|
-
if (!printStructured(ctx, result))
|
|
222
|
+
if (!printStructured(ctx, result)) {
|
|
223
|
+
if (result.lastRejectedStart) {
|
|
224
|
+
const rejection = result.lastRejectedStart;
|
|
225
|
+
ctx.print(`Last rejected start: ${rejection.at} · ${rejection.reason} · key ${rejection.keyId ?? "unknown"}`);
|
|
226
|
+
}
|
|
206
227
|
ctx.table(result.items, [
|
|
207
228
|
{ key: "keyId", label: "Key ID" },
|
|
208
229
|
{ key: "name", label: "Name" },
|
|
@@ -210,6 +231,7 @@ export const appKeyCommands = [
|
|
|
210
231
|
{ key: "status", label: "Status" },
|
|
211
232
|
{ key: "accountStatus", label: "Account" },
|
|
212
233
|
]);
|
|
234
|
+
}
|
|
213
235
|
},
|
|
214
236
|
}),
|
|
215
237
|
command("apps keys revoke", {
|