@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
|
@@ -507,6 +507,26 @@ const AppQueryClaimsSchema = z
|
|
|
507
507
|
.strict()
|
|
508
508
|
.refine((claims) => claims.nbf === claims.iat && claims.exp - claims.iat === CLOUD_INVOCATION_TOKEN_TTL_SECONDS);
|
|
509
509
|
|
|
510
|
+
const AppCapabilityClaimsSchema = z
|
|
511
|
+
.object({
|
|
512
|
+
iss: z.string().url(),
|
|
513
|
+
aud: z.string().regex(/^app:[a-z][a-z0-9-]{0,79}$/),
|
|
514
|
+
token_use: z.literal("app_capability"),
|
|
515
|
+
sub: AppIdSchema,
|
|
516
|
+
key_id: z.uuid(),
|
|
517
|
+
permission: z.string().min(1).max(100),
|
|
518
|
+
op: OperationSchema.refine((value) => value.startsWith("capability.query:") || value.startsWith("capability.action:")),
|
|
519
|
+
schema_hash: z.string().regex(/^[a-f0-9]{64}$/),
|
|
520
|
+
input_hash: z.string().regex(/^[a-f0-9]{64}$/),
|
|
521
|
+
request_id: BoundedIdentifierSchema,
|
|
522
|
+
jti: z.uuid(),
|
|
523
|
+
iat: z.number().int(),
|
|
524
|
+
nbf: z.number().int(),
|
|
525
|
+
exp: z.number().int(),
|
|
526
|
+
})
|
|
527
|
+
.strict()
|
|
528
|
+
.refine((claims) => claims.nbf === claims.iat && claims.exp - claims.iat === CLOUD_INVOCATION_TOKEN_TTL_SECONDS);
|
|
529
|
+
|
|
510
530
|
export const signAppQueryInvocation = async (input: {
|
|
511
531
|
workload: import("./workload-auth").AuthenticatedWorkload;
|
|
512
532
|
targetAppId: string;
|
|
@@ -595,3 +615,95 @@ export const verifyAppQueryInvocation = async (
|
|
|
595
615
|
throw error;
|
|
596
616
|
}
|
|
597
617
|
};
|
|
618
|
+
|
|
619
|
+
export const signAppCapabilityInvocation = async (input: {
|
|
620
|
+
app: { appId: string; keyId: string };
|
|
621
|
+
kind: "query" | "action";
|
|
622
|
+
permission: string;
|
|
623
|
+
targetAppId: string;
|
|
624
|
+
capabilityId: string;
|
|
625
|
+
schemaHash: string;
|
|
626
|
+
input: unknown;
|
|
627
|
+
requestId: string;
|
|
628
|
+
signer: PreparedIdentitySigner & { issuer: string };
|
|
629
|
+
}) => {
|
|
630
|
+
const now = Math.floor(Date.now() / 1000);
|
|
631
|
+
if (input.signer.signUntil.getTime() < (now + CLOUD_INVOCATION_TOKEN_TTL_SECONDS) * 1000)
|
|
632
|
+
throw new Error("Signing key expires before the app capability invocation");
|
|
633
|
+
const claims = AppCapabilityClaimsSchema.parse({
|
|
634
|
+
iss: input.signer.issuer,
|
|
635
|
+
aud: `app:${input.targetAppId}`,
|
|
636
|
+
token_use: "app_capability",
|
|
637
|
+
sub: input.app.appId,
|
|
638
|
+
key_id: input.app.keyId,
|
|
639
|
+
permission: input.permission,
|
|
640
|
+
op: `capability.${input.kind}:${input.capabilityId}`,
|
|
641
|
+
schema_hash: input.schemaHash,
|
|
642
|
+
input_hash: appEventPayloadHash(input.input),
|
|
643
|
+
request_id: input.requestId,
|
|
644
|
+
jti: crypto.randomUUID(),
|
|
645
|
+
iat: now,
|
|
646
|
+
nbf: now,
|
|
647
|
+
exp: now + CLOUD_INVOCATION_TOKEN_TTL_SECONDS,
|
|
648
|
+
});
|
|
649
|
+
const token = await new SignJWT(claims)
|
|
650
|
+
.setProtectedHeader({ alg: CLOUD_IDENTITY_ALGORITHM, typ: CLOUD_INVOCATION_TOKEN_TYPE, kid: input.signer.kid })
|
|
651
|
+
.sign(input.signer.key);
|
|
652
|
+
if (Buffer.byteLength(token) > IDENTITY_MAX_COMPACT_TOKEN_BYTES) throw new Error("App capability token exceeds its byte budget");
|
|
653
|
+
return { token, claims };
|
|
654
|
+
};
|
|
655
|
+
|
|
656
|
+
export const verifyAppCapabilityInvocation = async (
|
|
657
|
+
token: string,
|
|
658
|
+
expected: { appId: string; kind: "query" | "action"; capabilityId: string; schemaHash: string; input: unknown; permission: string },
|
|
659
|
+
options: { issuer?: string; key?: JWTVerifyGetKey; now?: Date } = {},
|
|
660
|
+
) => {
|
|
661
|
+
if (Buffer.byteLength(token) > IDENTITY_MAX_COMPACT_TOKEN_BYTES) return null;
|
|
662
|
+
try {
|
|
663
|
+
assertProtectedHeader(decodeProtectedHeader(token));
|
|
664
|
+
} catch {
|
|
665
|
+
return null;
|
|
666
|
+
}
|
|
667
|
+
// Configuration and key-fetch failures must remain retryable, rather than look like invalid tokens.
|
|
668
|
+
const runtime = options.issuer && options.key ? null : await getIdentityRuntimeConfig();
|
|
669
|
+
try {
|
|
670
|
+
const issuer = options.issuer ?? runtime!.issuer;
|
|
671
|
+
const state = options.key ? null : remoteSet(runtime!.invocationJwksUrl);
|
|
672
|
+
const key = options.key ?? state!.key;
|
|
673
|
+
const verified = await verifyWithKeyRefresh(
|
|
674
|
+
(candidate) =>
|
|
675
|
+
jwtVerify(token, candidate, {
|
|
676
|
+
algorithms: [CLOUD_IDENTITY_ALGORITHM],
|
|
677
|
+
issuer,
|
|
678
|
+
audience: `app:${expected.appId}`,
|
|
679
|
+
typ: CLOUD_INVOCATION_TOKEN_TYPE,
|
|
680
|
+
clockTolerance: INVOCATION_CLOCK_TOLERANCE_SECONDS,
|
|
681
|
+
currentDate: options.now,
|
|
682
|
+
}),
|
|
683
|
+
key,
|
|
684
|
+
state ? { url: runtime!.invocationJwksUrl, state } : null,
|
|
685
|
+
);
|
|
686
|
+
const claims = AppCapabilityClaimsSchema.parse(verified.payload);
|
|
687
|
+
if (
|
|
688
|
+
claims.permission !== expected.permission ||
|
|
689
|
+
claims.op !== `capability.${expected.kind}:${expected.capabilityId}` ||
|
|
690
|
+
claims.schema_hash !== expected.schemaHash ||
|
|
691
|
+
claims.input_hash !== appEventPayloadHash(expected.input)
|
|
692
|
+
)
|
|
693
|
+
return null;
|
|
694
|
+
return claims;
|
|
695
|
+
} catch (error) {
|
|
696
|
+
if (
|
|
697
|
+
error instanceof z.ZodError ||
|
|
698
|
+
error instanceof errors.JWTExpired ||
|
|
699
|
+
error instanceof errors.JWTClaimValidationFailed ||
|
|
700
|
+
error instanceof errors.JWTInvalid ||
|
|
701
|
+
error instanceof errors.JWSInvalid ||
|
|
702
|
+
error instanceof errors.JWSSignatureVerificationFailed ||
|
|
703
|
+
error instanceof errors.JWKSNoMatchingKey ||
|
|
704
|
+
error instanceof errors.JOSENotSupported
|
|
705
|
+
)
|
|
706
|
+
return null;
|
|
707
|
+
throw error;
|
|
708
|
+
}
|
|
709
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { crypto } from "@k2b/stdlib";
|
|
2
|
-
import { sql } from "bun";
|
|
3
2
|
import { CompactSign, compactVerify, exportJWK, generateKeyPair, importJWK, type JWK } from "jose";
|
|
3
|
+
import { sql } from "../../runtime/clients";
|
|
4
4
|
import { logger } from "../logging";
|
|
5
5
|
import {
|
|
6
6
|
CLOUD_IDENTITY_ALGORITHM,
|
|
@@ -449,7 +449,7 @@ export const initializeIdentityAuthority = async (): Promise<void> => {
|
|
|
449
449
|
if (keys.previous) await rewrapIdentitySigningKeys();
|
|
450
450
|
};
|
|
451
451
|
|
|
452
|
-
export const listIdentityJwks = async (purpose:
|
|
452
|
+
export const listIdentityJwks = async (purpose: SigningKeyPurpose): Promise<{ keys: JWK[]; etag: string }> => {
|
|
453
453
|
const rows = await sql<Array<{ public_jwk: JWK | string }>>`
|
|
454
454
|
SELECT public_jwk
|
|
455
455
|
FROM auth.signing_keys
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { sql } from "bun";
|
|
2
1
|
import { z } from "zod";
|
|
3
2
|
import { type ResourcePrincipal, ResourcePrincipalSchema } from "../../contracts/capabilities";
|
|
4
3
|
import type { AccessSubject } from "../../contracts/shared";
|
|
4
|
+
import { sql } from "../../runtime/clients";
|
|
5
5
|
import { decodeAccountCategoryEnabled } from "../account-category-policy";
|
|
6
6
|
import { toPgUuidArray } from "../postgres";
|
|
7
7
|
import type { ServiceAccount } from "../service-accounts";
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import { type Context, Hono } from "hono";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
CLOUD_INTERNAL_OAUTH_JWKS_PATH,
|
|
4
|
+
CLOUD_INVOCATION_JWKS_PATH,
|
|
5
|
+
CLOUD_SESSION_JWKS_PATH,
|
|
6
|
+
IDENTITY_JWKS_MAX_AGE_SECONDS,
|
|
7
|
+
} from "./constants";
|
|
3
8
|
import { listIdentityJwks } from "./key-ring";
|
|
4
9
|
|
|
5
10
|
// If-None-Match uses weak comparison, including lists and the wildcard.
|
|
6
11
|
const matchesEtag = (header: string | undefined, etag: string): boolean =>
|
|
7
12
|
header?.trim() === "*" || (header?.match(/(?:W\/)?"[^"]*"/g) ?? []).some((tag) => tag.replace(/^W\//, "") === etag.replace(/^W\//, ""));
|
|
8
13
|
|
|
9
|
-
const serveJwks = (purpose: "session" | "invocation") => async (c: Context) => {
|
|
14
|
+
const serveJwks = (purpose: "session" | "invocation" | "oauth") => async (c: Context) => {
|
|
10
15
|
const jwks = await listIdentityJwks(purpose);
|
|
11
16
|
if (matchesEtag(c.req.header("if-none-match"), jwks.etag)) {
|
|
12
17
|
c.header("ETag", jwks.etag);
|
|
@@ -19,4 +24,7 @@ const serveJwks = (purpose: "session" | "invocation") => async (c: Context) => {
|
|
|
19
24
|
};
|
|
20
25
|
|
|
21
26
|
export const createIdentityPublicRoutes = (): Hono =>
|
|
22
|
-
new Hono()
|
|
27
|
+
new Hono()
|
|
28
|
+
.get(CLOUD_SESSION_JWKS_PATH, serveJwks("session"))
|
|
29
|
+
.get(CLOUD_INVOCATION_JWKS_PATH, serveJwks("invocation"))
|
|
30
|
+
.get(CLOUD_INTERNAL_OAUTH_JWKS_PATH, serveJwks("oauth"));
|
|
@@ -4,7 +4,13 @@ import { publicCloudOrigin } from "../../shared/app-url";
|
|
|
4
4
|
import { get as getSetting } from "../settings";
|
|
5
5
|
import { decryptValue } from "../settings/crypto";
|
|
6
6
|
import { SETTINGS_MAP, validateSettingValue } from "../settings/defaults";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
CLOUD_INTERNAL_OAUTH_JWKS_PATH,
|
|
9
|
+
CLOUD_INVOCATION_JWKS_PATH,
|
|
10
|
+
CLOUD_OAUTH_JWKS_PATH,
|
|
11
|
+
CLOUD_SESSION_JWKS_PATH,
|
|
12
|
+
IDENTITY_REFRESH_TIMEOUT_MS,
|
|
13
|
+
} from "./constants";
|
|
8
14
|
|
|
9
15
|
const RUNTIME_CONFIG_TTL_MS = 60_000;
|
|
10
16
|
|
|
@@ -75,8 +81,13 @@ export const resolveInvocationJwksUrl = (
|
|
|
75
81
|
transportOrigin = env.CLOUD_IDENTITY_JWKS_ORIGIN ?? env.CLOUD_CORE_INTERNAL_ORIGIN,
|
|
76
82
|
): URL => resolveCoreJwksUrl(issuer, CLOUD_INVOCATION_JWKS_PATH, transportOrigin);
|
|
77
83
|
|
|
78
|
-
export const resolveOAuthJwksUrl = (
|
|
79
|
-
|
|
84
|
+
export const resolveOAuthJwksUrl = (
|
|
85
|
+
issuer: string,
|
|
86
|
+
transportOrigin = env.CLOUD_OAUTH_JWKS_ORIGIN,
|
|
87
|
+
coreOrigin = env.CLOUD_CORE_INTERNAL_ORIGIN,
|
|
88
|
+
): URL => {
|
|
89
|
+
if (!transportOrigin)
|
|
90
|
+
return coreOrigin ? resolveCoreJwksUrl(issuer, CLOUD_INTERNAL_OAUTH_JWKS_PATH, coreOrigin) : new URL(CLOUD_OAUTH_JWKS_PATH, issuer);
|
|
80
91
|
|
|
81
92
|
const url = new URL(CLOUD_OAUTH_JWKS_PATH, transportOrigin);
|
|
82
93
|
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
@@ -9,6 +9,8 @@ export type AuthenticatedWorkload = {
|
|
|
9
9
|
serviceAccountId: string;
|
|
10
10
|
credentialId: string;
|
|
11
11
|
scope: WorkloadScope;
|
|
12
|
+
/** Present only for a verified signed app request, never a legacy token. */
|
|
13
|
+
keyId?: string;
|
|
12
14
|
};
|
|
13
15
|
|
|
14
16
|
type WorkloadCredentialAuthenticator = Pick<typeof serviceAccountCredentials, "isApiToken" | "authenticateApiToken">;
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/** Core-only workload issuer. This module never reads identity signing keys, OAuth secrets, or sessions. */
|
|
2
2
|
import type { NatsConnection } from "@nats-io/transport-node";
|
|
3
|
-
import { sql } from "bun";
|
|
4
3
|
import { getProcessNats } from "../../_internal/process-sync";
|
|
5
4
|
import { APP_REGISTRY_TTL_MS, appInstancePrefix, appInstanceRegistry } from "../../_internal/registry";
|
|
6
5
|
import {
|
|
@@ -12,7 +11,8 @@ import {
|
|
|
12
11
|
type WorkloadResponse,
|
|
13
12
|
workloadSubject,
|
|
14
13
|
} from "../../_internal/workload-protocol";
|
|
15
|
-
import {
|
|
14
|
+
import { sql } from "../../runtime/clients";
|
|
15
|
+
import { connections } from "../../runtime/connections";
|
|
16
16
|
import { audit } from "../audit";
|
|
17
17
|
import { logger } from "../logging";
|
|
18
18
|
import { serviceAccountCredentials } from "../service-account-credentials";
|
|
@@ -204,7 +204,7 @@ export const startAppWorkloadBroker = async (
|
|
|
204
204
|
let stopped = false;
|
|
205
205
|
let maintenance: Promise<void> | undefined;
|
|
206
206
|
let timer: Timer | undefined;
|
|
207
|
-
const subscription = connection.subscribe(workloadSubject(options.namespace ??
|
|
207
|
+
const subscription = connection.subscribe(workloadSubject(options.namespace ?? connections().nats.namespace), {
|
|
208
208
|
queue: "core-workload-v1",
|
|
209
209
|
callback: (error, message) => {
|
|
210
210
|
if (error || !message.reply) return;
|
package/src/services/index.ts
CHANGED
|
@@ -253,6 +253,7 @@ export type { NetworkLookup, PublicNetworkAddress } from "./network-security";
|
|
|
253
253
|
|
|
254
254
|
export { geoip } from "./geoip";
|
|
255
255
|
export { appDetails } from "./app-details";
|
|
256
|
+
export { appAdminFacts, type AppAdminFacts } from "./app-admin-facts";
|
|
256
257
|
export {
|
|
257
258
|
knownBrowsers,
|
|
258
259
|
KNOWN_BROWSER_LIMITS,
|
|
@@ -294,3 +295,6 @@ export type {
|
|
|
294
295
|
ObjectListOptions,
|
|
295
296
|
ObjectListResult,
|
|
296
297
|
} from "./object-store";
|
|
298
|
+
|
|
299
|
+
export { links } from "./links";
|
|
300
|
+
export type { CreateLinkInput, CreatedLink, RedeemedLink, LinkOptions, PeekedLink, PeekLinkResult, RedeemLinkResult } from "./links";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { sql } from "bun";
|
|
2
1
|
import type { BaseGroup, GroupMember, MutationResult } from "../../contracts/shared";
|
|
2
|
+
import { sql } from "../../runtime/clients";
|
|
3
3
|
import { freeipa } from "../../server/services";
|
|
4
4
|
import { toPgUuidArray } from "../postgres";
|
|
5
5
|
import { ensureFreeIpaMutationAvailable, getIpaUrl } from "./guard";
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* Local group mutations keep that projection as source of truth; the local
|
|
6
6
|
* display mirror is only a bootstrap fallback before the first full sync.
|
|
7
7
|
*/
|
|
8
|
+
import { sql } from "../../runtime/clients";
|
|
8
9
|
|
|
9
|
-
import { sql } from "bun";
|
|
10
10
|
import { calculateIpaProfileFromGroupNames } from "../account-model";
|
|
11
11
|
import { getFreeIpaConfig } from "../freeipa-config";
|
|
12
12
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { sql } from "bun";
|
|
2
1
|
import type { BaseGroup, BaseUser, UserProfile, UserProvider } from "../../contracts/shared";
|
|
2
|
+
import { sql } from "../../runtime/clients";
|
|
3
3
|
import { freeipa } from "../../server/services";
|
|
4
4
|
import { buildRoles } from "../account-model";
|
|
5
5
|
import { getFreeIpaConfig } from "../freeipa-config";
|
package/src/services/ipa/sync.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { sql } from "bun";
|
|
2
1
|
import { coreUserEventPublisher } from "../../events/core-user-created";
|
|
2
|
+
import { sql } from "../../runtime/clients";
|
|
3
3
|
import { freeipa } from "../../server/services";
|
|
4
4
|
import { writeDeletedAccountAudit } from "../account-lifecycle/audit";
|
|
5
5
|
import { calculateIpaProfileFromGroupNames, parseIpaAccountTransitionPolicy, parseIpaMatchMode } from "../account-model";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { password } from "@k2b/stdlib";
|
|
2
|
-
import { sql } from "bun";
|
|
3
2
|
import type { MutationResult, UserProfile } from "../../contracts/shared";
|
|
4
3
|
import { coreUserEventPublisher } from "../../events/core-user-created";
|
|
4
|
+
import { sql } from "../../runtime/clients";
|
|
5
5
|
import { freeipa } from "../../server/services";
|
|
6
6
|
import { writeDeletedAccountAudit } from "../account-lifecycle/audit";
|
|
7
7
|
import { resolveProviderProfile } from "../accounts/base-user";
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { SQL } from "bun";
|
|
2
2
|
import type { Context } from "hono";
|
|
3
3
|
import { getCookie, setCookie } from "hono/cookie";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { env } from "../config/env";
|
|
6
6
|
import { APP_LOGIN_CONTEXT_BROWSERS, APP_LOGIN_CONTEXT_OSES } from "../contracts/app-approval";
|
|
7
|
+
import { sql } from "../runtime/clients";
|
|
7
8
|
import { type BrowserCode, type OsCode, truncateUtf8 } from "../server/request-context";
|
|
8
9
|
import { logger } from "./logging";
|
|
9
10
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { sql } from "bun";
|
|
3
2
|
import type { Context } from "hono";
|
|
4
3
|
import { HTTPException } from "hono/http-exception";
|
|
4
|
+
import { sql } from "../runtime/clients";
|
|
5
5
|
import { isAccountCategoryAllowed } from "./account-category-policy";
|
|
6
6
|
import { isAccountExpired } from "./account-model";
|
|
7
7
|
import { verifySessionToken } from "./identity";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { getProcessApplicationPackage } from "../../_internal/process-identity";
|
|
2
|
+
import { sql } from "../../runtime/clients";
|
|
3
|
+
import { type CreateLinkInput, createLinksStore, type LinkOptions } from "./store";
|
|
4
|
+
|
|
5
|
+
const store = () => {
|
|
6
|
+
const packageName = getProcessApplicationPackage();
|
|
7
|
+
if (!packageName) throw new Error("Start the application before using one-time links");
|
|
8
|
+
return createLinksStore(sql, packageName);
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const links = {
|
|
12
|
+
create: (input: CreateLinkInput, options?: LinkOptions) => store().create(input, options),
|
|
13
|
+
peek: (token: string, input: { purpose: string }, options?: LinkOptions) => store().peek(token, input, options),
|
|
14
|
+
redeem: (token: string, input: { purpose: string }, options?: LinkOptions) => store().redeem(token, input, options),
|
|
15
|
+
};
|
|
16
|
+
export type { CreatedLink, CreateLinkInput, LinkOptions, PeekedLink, PeekLinkResult, RedeemedLink, RedeemLinkResult } from "./store";
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { lazySync } from "../../_internal/process-sync";
|
|
2
|
+
import { sql } from "../../runtime/clients";
|
|
3
|
+
import { logger } from "../logging";
|
|
4
|
+
import { cleanupLinksBatch, LINK_CLEANUP_BATCH, setupLinks } from "./store";
|
|
5
|
+
|
|
6
|
+
// All apps compete for one platform schedule, including replicas and apps sharing a package.
|
|
7
|
+
const scheduler = lazySync((sync) =>
|
|
8
|
+
sync.scheduler({
|
|
9
|
+
id: "cloud-links-retention",
|
|
10
|
+
owner: "platform",
|
|
11
|
+
delivery: { maxAttempts: 2, backoffMs: [30_000], ackWaitMs: 60_000 },
|
|
12
|
+
}),
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
/** Provision before app setup and readiness, then drain this worker before NATS during shutdown. */
|
|
16
|
+
export const startLinksRuntime = async (): Promise<() => Promise<void>> => {
|
|
17
|
+
await setupLinks(sql);
|
|
18
|
+
const stopping = new AbortController();
|
|
19
|
+
await scheduler().create({
|
|
20
|
+
id: "retention",
|
|
21
|
+
cron: "0 * * * *",
|
|
22
|
+
timezone: "UTC",
|
|
23
|
+
misfire: "latest",
|
|
24
|
+
meta: { family: "links", label: "One-time link cleanup" },
|
|
25
|
+
process: async ({ signal, heartbeat }) => {
|
|
26
|
+
const runSignal = AbortSignal.any([signal, stopping.signal]);
|
|
27
|
+
const deadline = performance.now() + 5 * 60_000;
|
|
28
|
+
let deleted = 0;
|
|
29
|
+
while (!runSignal.aborted && performance.now() < deadline) {
|
|
30
|
+
const count = await cleanupLinksBatch(sql);
|
|
31
|
+
deleted += count;
|
|
32
|
+
if (count < LINK_CLEANUP_BATCH) break;
|
|
33
|
+
await heartbeat();
|
|
34
|
+
}
|
|
35
|
+
if (deleted > 0) logger("links:cleanup").info("Removed old one-time links", { deleted });
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
const worker = await scheduler().process({ concurrency: 1 });
|
|
39
|
+
return async () => {
|
|
40
|
+
stopping.abort();
|
|
41
|
+
worker.stop();
|
|
42
|
+
await worker.drain({ timeoutMs: 30_000 });
|
|
43
|
+
};
|
|
44
|
+
};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import type { SQL, TransactionSQL } from "bun";
|
|
3
|
+
import { isPurposeName, validatePurposeName } from "../../_internal/purpose-name";
|
|
4
|
+
import { ensureSchema } from "../postgres";
|
|
5
|
+
|
|
6
|
+
export type CreateLinkInput = { purpose: string; subject: string; ttl: number };
|
|
7
|
+
export type CreatedLink = { token: string; expiresAt: Date };
|
|
8
|
+
export type RedeemedLink = { subject: string; createdAt: Date };
|
|
9
|
+
export type PeekedLink = RedeemedLink & { expiresAt: Date };
|
|
10
|
+
export type LinkOptions = { tx?: TransactionSQL };
|
|
11
|
+
type LinkResult<T> = { ok: true; data: T } | { ok: false; error: { code: "LINK_INVALID" } };
|
|
12
|
+
export type RedeemLinkResult = LinkResult<RedeemedLink>;
|
|
13
|
+
export type PeekLinkResult = LinkResult<PeekedLink>;
|
|
14
|
+
|
|
15
|
+
const TOKEN = /^cl_[A-Za-z0-9_-]{42}[AEIMQUYcgkosw048]$/;
|
|
16
|
+
// Match the existing outgoing-mail retention batch budget.
|
|
17
|
+
export const LINK_CLEANUP_BATCH = 1000;
|
|
18
|
+
const hashToken = (token: string): string => createHash("sha256").update(token).digest("hex");
|
|
19
|
+
|
|
20
|
+
const linkCandidate = (token: string, purpose: string) => {
|
|
21
|
+
const value = typeof token === "string" ? token : "";
|
|
22
|
+
const digest = hashToken(value);
|
|
23
|
+
const validPurpose = isPurposeName(purpose);
|
|
24
|
+
return { digest, purpose: validPurpose ? purpose : "", valid: TOKEN.test(value) && validPurpose };
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const validateCreateLink = (input: CreateLinkInput): void => {
|
|
28
|
+
validatePurposeName(input.purpose);
|
|
29
|
+
if (typeof input.subject !== "string" || Buffer.byteLength(input.subject, "utf8") > 1024) {
|
|
30
|
+
throw new TypeError("Link subject must be a string of at most 1024 UTF-8 bytes");
|
|
31
|
+
}
|
|
32
|
+
if (!Number.isFinite(input.ttl) || input.ttl < 60 || input.ttl > 30 * 24 * 60 * 60) {
|
|
33
|
+
throw new RangeError("Link ttl must be seconds from 60 to 2592000 (30 days)");
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** One reserved platform table in the same database as the application's domain transactions. */
|
|
38
|
+
export const setupLinks = async (db: SQL): Promise<void> => {
|
|
39
|
+
await db.begin(async (tx) => {
|
|
40
|
+
await tx`SELECT pg_advisory_xact_lock(hashtext('cloud.links'))`;
|
|
41
|
+
await ensureSchema(tx, "platform");
|
|
42
|
+
const [existing] = await tx<{ table_exists: boolean; index_exists: boolean }[]>`
|
|
43
|
+
SELECT to_regclass('platform._cloud_links') IS NOT NULL AS table_exists,
|
|
44
|
+
to_regclass('platform._cloud_links_retention') IS NOT NULL AS index_exists`;
|
|
45
|
+
if (!existing?.table_exists)
|
|
46
|
+
await tx`CREATE TABLE IF NOT EXISTS platform._cloud_links (
|
|
47
|
+
package text NOT NULL, token_hash text NOT NULL, purpose text NOT NULL, subject bytea NOT NULL,
|
|
48
|
+
created_at timestamptz NOT NULL, expires_at timestamptz NOT NULL, used_at timestamptz,
|
|
49
|
+
PRIMARY KEY (package, token_hash)
|
|
50
|
+
)`.simple();
|
|
51
|
+
// Even IF NOT EXISTS takes a ShareLock; skip index DDL on ordinary app restarts.
|
|
52
|
+
if (!existing?.index_exists)
|
|
53
|
+
await tx`CREATE INDEX IF NOT EXISTS _cloud_links_retention ON platform._cloud_links (LEAST(expires_at, used_at))`.simple();
|
|
54
|
+
});
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export const createLinksStore = (db: SQL, packageName: string) => ({
|
|
58
|
+
create: async (input: CreateLinkInput, options: LinkOptions = {}): Promise<CreatedLink> => {
|
|
59
|
+
validateCreateLink(input);
|
|
60
|
+
const token = `cl_${randomBytes(32).toString("base64url")}`;
|
|
61
|
+
const [row] = await (options.tx ?? db)<{ expires_at: Date }[]>`
|
|
62
|
+
INSERT INTO platform._cloud_links (package, token_hash, purpose, subject, created_at, expires_at)
|
|
63
|
+
VALUES (${packageName}, ${hashToken(token)}, ${input.purpose}, ${Buffer.from(input.subject, "utf8")},
|
|
64
|
+
statement_timestamp(), statement_timestamp() + ${input.ttl} * interval '1 second')
|
|
65
|
+
RETURNING expires_at`;
|
|
66
|
+
if (!row) throw new Error("One-time link insert returned no expiry");
|
|
67
|
+
return { token, expiresAt: row.expires_at };
|
|
68
|
+
},
|
|
69
|
+
peek: async (token: string, input: { purpose: string }, options: LinkOptions = {}): Promise<PeekLinkResult> => {
|
|
70
|
+
const candidate = linkCandidate(token, input.purpose);
|
|
71
|
+
const [row] = await (options.tx ?? db)<{ subject: Uint8Array; created_at: Date; expires_at: Date }[]>`
|
|
72
|
+
SELECT subject, created_at, expires_at FROM platform._cloud_links
|
|
73
|
+
WHERE package = ${packageName} AND token_hash = ${candidate.digest} AND purpose = ${candidate.purpose}
|
|
74
|
+
AND ${candidate.valid}::boolean AND used_at IS NULL AND expires_at > clock_timestamp()`;
|
|
75
|
+
return row
|
|
76
|
+
? { ok: true, data: { subject: Buffer.from(row.subject).toString("utf8"), createdAt: row.created_at, expiresAt: row.expires_at } }
|
|
77
|
+
: { ok: false, error: { code: "LINK_INVALID" } };
|
|
78
|
+
},
|
|
79
|
+
redeem: async (token: string, input: { purpose: string }, options: LinkOptions = {}): Promise<RedeemLinkResult> => {
|
|
80
|
+
// Every candidate is hashed and reaches one indexed UPDATE, including malformed credentials.
|
|
81
|
+
const candidate = linkCandidate(token, input.purpose);
|
|
82
|
+
const [row] = await (options.tx ?? db)<{ subject: Uint8Array; created_at: Date }[]>`
|
|
83
|
+
UPDATE platform._cloud_links SET used_at = clock_timestamp()
|
|
84
|
+
WHERE package = ${packageName} AND token_hash = ${candidate.digest} AND purpose = ${candidate.purpose}
|
|
85
|
+
AND ${candidate.valid}::boolean AND used_at IS NULL AND expires_at > clock_timestamp()
|
|
86
|
+
RETURNING subject, created_at`;
|
|
87
|
+
return row
|
|
88
|
+
? { ok: true, data: { subject: Buffer.from(row.subject).toString("utf8"), createdAt: row.created_at } }
|
|
89
|
+
: { ok: false, error: { code: "LINK_INVALID" } };
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
/** Platform maintenance spans packages; the application facade never exposes this operation. */
|
|
94
|
+
export const cleanupLinksBatch = async (db: SQL): Promise<number> =>
|
|
95
|
+
db.begin(async (tx) => {
|
|
96
|
+
await tx`SET LOCAL statement_timeout = '10s'`;
|
|
97
|
+
const rows = await tx<{ token_hash: string }[]>`
|
|
98
|
+
WITH expired AS (
|
|
99
|
+
SELECT package, token_hash FROM platform._cloud_links
|
|
100
|
+
WHERE LEAST(expires_at, used_at) < statement_timestamp() - interval '24 hours'
|
|
101
|
+
ORDER BY LEAST(expires_at, used_at) LIMIT ${LINK_CLEANUP_BATCH} FOR UPDATE SKIP LOCKED
|
|
102
|
+
) DELETE FROM platform._cloud_links link USING expired
|
|
103
|
+
WHERE link.package = expired.package AND link.token_hash = expired.token_hash RETURNING link.token_hash`;
|
|
104
|
+
return rows.length;
|
|
105
|
+
});
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { sql } from "bun";
|
|
2
1
|
import type { PaginationParams } from "../../contracts/shared";
|
|
2
|
+
import { sql } from "../../runtime/clients";
|
|
3
3
|
import { escapeLikePattern, parsePgJsonRecord, toPgTextArray } from "../postgres";
|
|
4
4
|
import { registerSettings } from "../settings/defaults";
|
|
5
|
-
import { redactMetadata } from "./redaction";
|
|
5
|
+
import { redactLinkTokens, redactMetadata } from "./redaction";
|
|
6
6
|
import { observeSyncEvent, TRACE_STUCK_AFTER_MS, trace } from "./trace";
|
|
7
7
|
import { readWebVitals } from "./web-vitals";
|
|
8
8
|
|
|
@@ -120,19 +120,24 @@ const mapRow = (row: DbLogRow): LogEntry => ({
|
|
|
120
120
|
/** Fire-and-forget write — mirrors to console, then inserts to DB async. */
|
|
121
121
|
function write(params: WriteParams): void {
|
|
122
122
|
const safeMetadata = params.metadata ? (redactMetadata(params.metadata) as Record<string, unknown>) : undefined;
|
|
123
|
-
const
|
|
123
|
+
const source = redactLinkTokens(params.source);
|
|
124
|
+
const message = redactLinkTokens(params.message);
|
|
125
|
+
const prefix = `[${source}]`;
|
|
124
126
|
const consoleFn = params.level === "error" ? console.error : params.level === "warn" ? console.warn : console.log;
|
|
125
|
-
consoleFn(prefix,
|
|
127
|
+
consoleFn(prefix, message, ...(safeMetadata ? [safeMetadata] : []));
|
|
126
128
|
|
|
127
|
-
|
|
129
|
+
const persist = async () => {
|
|
130
|
+
await sql`
|
|
128
131
|
INSERT INTO logging.entries (level, source, message, metadata)
|
|
129
132
|
VALUES (
|
|
130
133
|
${params.level},
|
|
131
|
-
${
|
|
132
|
-
${
|
|
134
|
+
${source},
|
|
135
|
+
${message},
|
|
133
136
|
(${safeMetadata ? JSON.stringify(safeMetadata) : null}::text)::jsonb
|
|
134
137
|
)
|
|
135
|
-
|
|
138
|
+
`;
|
|
139
|
+
};
|
|
140
|
+
void persist().catch((err: Error) => console.error("[logging] DB write failed:", err.message));
|
|
136
141
|
}
|
|
137
142
|
|
|
138
143
|
/**
|
|
@@ -7,14 +7,18 @@ const SENSITIVE_KEY_PATTERN = /(password|secret|token|cookie|authorization|api[_
|
|
|
7
7
|
|
|
8
8
|
export const REDACTED = "[REDACTED]";
|
|
9
9
|
|
|
10
|
+
/** Recognizable one-time credentials are scrubbed even under ordinary metadata keys. */
|
|
11
|
+
export const redactLinkTokens = (value: string): string => value.replace(/cl_[A-Za-z0-9_-]{43}/g, REDACTED);
|
|
12
|
+
|
|
10
13
|
export const isSensitiveMetadataKey = (key: string): boolean => SENSITIVE_KEY_PATTERN.test(key);
|
|
11
14
|
|
|
12
15
|
export const redactMetadata = (input: unknown): unknown => {
|
|
16
|
+
if (typeof input === "string") return redactLinkTokens(input);
|
|
13
17
|
if (input === null || typeof input !== "object") return input;
|
|
14
18
|
if (Array.isArray(input)) return input.map(redactMetadata);
|
|
15
19
|
const out: Record<string, unknown> = {};
|
|
16
20
|
for (const [key, value] of Object.entries(input)) {
|
|
17
|
-
out[key] = isSensitiveMetadataKey(key) ? REDACTED : redactMetadata(value);
|
|
21
|
+
out[redactLinkTokens(key)] = isSensitiveMetadataKey(key) ? REDACTED : redactMetadata(value);
|
|
18
22
|
}
|
|
19
23
|
return out;
|
|
20
24
|
};
|
|
@@ -60,6 +64,7 @@ export const redactSensitivePath = (pathname: string): string => {
|
|
|
60
64
|
.split("/")
|
|
61
65
|
.map((segment) => {
|
|
62
66
|
const value = decodeSegment(segment);
|
|
67
|
+
if (redactLinkTokens(value) !== value) return ":token";
|
|
63
68
|
if (UUID.test(value)) return ":id";
|
|
64
69
|
// Dot-separated parts cover signed tokens and credential filenames such as .ics.
|
|
65
70
|
// Lowercase word slugs preserve readable routes like getting-started-with-grids.
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { createHash, randomBytes } from "node:crypto";
|
|
2
2
|
import type { SyncEvent } from "@k2b/sync";
|
|
3
|
-
import { sql } from "bun";
|
|
4
3
|
import { getProcessApplicationId } from "../../_internal/process-identity";
|
|
5
4
|
import type { PaginationParams } from "../../contracts/shared";
|
|
5
|
+
import { sql } from "../../runtime/clients";
|
|
6
6
|
import { escapeLikePattern, parsePgJsonRecord, toPgTextArray } from "../postgres";
|
|
7
|
-
import { isSensitiveMetadataKey, REDACTED, redactMetadata } from "./redaction";
|
|
7
|
+
import { isSensitiveMetadataKey, REDACTED, redactLinkTokens, redactMetadata } from "./redaction";
|
|
8
8
|
|
|
9
9
|
export type TraceAttributeValue = string | number | boolean | null | undefined;
|
|
10
10
|
export type TraceAttributes = Record<string, TraceAttributeValue>;
|
|
@@ -499,10 +499,10 @@ const start = async (params: TraceStartParams, preservePresentation = false): Pr
|
|
|
499
499
|
VALUES (
|
|
500
500
|
${context.traceId},
|
|
501
501
|
${context.spanId},
|
|
502
|
-
${params.spanKey
|
|
502
|
+
${params.spanKey ? redactLinkTokens(params.spanKey) : null},
|
|
503
503
|
${params.parent?.spanId ?? null},
|
|
504
|
-
${params.name},
|
|
505
|
-
${params.source},
|
|
504
|
+
${redactLinkTokens(params.name)},
|
|
505
|
+
${redactLinkTokens(params.source)},
|
|
506
506
|
${params.appId ?? null},
|
|
507
507
|
${params.category ?? "custom"},
|
|
508
508
|
${params.kind ?? "internal"},
|
|
@@ -567,10 +567,10 @@ const complete = async (params: TraceCompleteParams, preservePresentation = fals
|
|
|
567
567
|
VALUES (
|
|
568
568
|
${context.traceId},
|
|
569
569
|
${context.spanId},
|
|
570
|
-
${params.spanKey
|
|
570
|
+
${params.spanKey ? redactLinkTokens(params.spanKey) : null},
|
|
571
571
|
${params.parent?.spanId ?? null},
|
|
572
|
-
${params.name},
|
|
573
|
-
${params.source},
|
|
572
|
+
${redactLinkTokens(params.name)},
|
|
573
|
+
${redactLinkTokens(params.source)},
|
|
574
574
|
${params.appId ?? null},
|
|
575
575
|
${params.category ?? "custom"},
|
|
576
576
|
${params.kind ?? "internal"},
|
|
@@ -636,7 +636,7 @@ const record = async (params: TraceRecordParams, preservePresentation = false):
|
|
|
636
636
|
VALUES (
|
|
637
637
|
${context.traceId},
|
|
638
638
|
${context.spanId},
|
|
639
|
-
${params.event},
|
|
639
|
+
${redactLinkTokens(params.event)},
|
|
640
640
|
${params.severity ?? "info"},
|
|
641
641
|
(${attributesJson}::text)::jsonb,
|
|
642
642
|
${body},
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { err, fail, ok, type PageParams, type Paginated, paginate, type Result } from "@k2b/stdlib";
|
|
2
|
-
import {
|
|
2
|
+
import type { SQL } from "bun";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import type { CapabilityActionPolicy } from "../../contracts/capabilities";
|
|
5
5
|
import { CapabilityAppIdSchema } from "../../contracts/capabilities";
|
|
6
|
+
import { sql } from "../../runtime/clients";
|
|
6
7
|
import { isAccountCategoryAllowed } from "../account-category-policy";
|
|
7
8
|
import { audit } from "../audit";
|
|
8
9
|
import { requireRegisteredAppAccess, resolveIssuanceAuthority } from "../identity/issue-invocation";
|