@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
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import type { AppKey } from "../app-key";
|
|
2
|
+
import {
|
|
3
|
+
BOOTSTRAP_ATTEMPT_MS,
|
|
4
|
+
BOOTSTRAP_BODY,
|
|
5
|
+
BOOTSTRAP_BUDGET_MS,
|
|
6
|
+
BOOTSTRAP_PATH,
|
|
7
|
+
BOOTSTRAP_RESPONSE_BYTES,
|
|
8
|
+
type BootstrapResponse,
|
|
9
|
+
BootstrapResponseSchema,
|
|
10
|
+
signBootstrapProof,
|
|
11
|
+
} from "./bootstrap-protocol";
|
|
12
|
+
|
|
13
|
+
export class BootstrapError extends Error {
|
|
14
|
+
constructor(
|
|
15
|
+
readonly kind: "transient" | "permanent",
|
|
16
|
+
readonly reason: string,
|
|
17
|
+
readonly retryAfterMs = 0,
|
|
18
|
+
) {
|
|
19
|
+
super(`Core bootstrap ${kind}: ${reason}`);
|
|
20
|
+
this.name = "BootstrapError";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const originUrl = (origin: string | undefined): URL => {
|
|
25
|
+
try {
|
|
26
|
+
if (!origin) throw new Error();
|
|
27
|
+
const url = new URL(origin);
|
|
28
|
+
if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.pathname !== "/" || url.search || url.hash)
|
|
29
|
+
throw new Error();
|
|
30
|
+
return new URL(BOOTSTRAP_PATH, url);
|
|
31
|
+
} catch {
|
|
32
|
+
throw new BootstrapError("permanent", "CLOUD_CORE_INTERNAL_ORIGIN must be a direct http(s) Core origin");
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
const httpReason = (status: number): string => {
|
|
36
|
+
if (status === 401) return "HTTP 401: unknown, revoked, expired or invalid app key/proof";
|
|
37
|
+
if (status === 403) return "HTTP 403: connections_not_permitted, app/installation mismatch or replayed proof";
|
|
38
|
+
if (status === 404) return "HTTP 404: bootstrap route unavailable; check the direct Core origin and version";
|
|
39
|
+
return `HTTP ${status}`;
|
|
40
|
+
};
|
|
41
|
+
const retryAfter = (header: string | null, now: number): number => {
|
|
42
|
+
if (!header) return 0;
|
|
43
|
+
if (/^\d+$/.test(header)) return Number(header) * 1000;
|
|
44
|
+
const date = Date.parse(header);
|
|
45
|
+
return Number.isFinite(date) ? Math.max(0, date - now) : 0;
|
|
46
|
+
};
|
|
47
|
+
// Keep transport read failures separate from invalid JSON: only transport failures retry.
|
|
48
|
+
const readResponse = async (response: Response, signal: AbortSignal): Promise<unknown> => {
|
|
49
|
+
const tooLarge = () => new BootstrapError("permanent", "response too_large");
|
|
50
|
+
if (Number(response.headers.get("Content-Length")) > BOOTSTRAP_RESPONSE_BYTES) {
|
|
51
|
+
void response.body?.cancel().catch(() => undefined);
|
|
52
|
+
throw tooLarge();
|
|
53
|
+
}
|
|
54
|
+
const reader = response.body?.getReader();
|
|
55
|
+
if (!reader) throw new BootstrapError("permanent", "response invalid_json");
|
|
56
|
+
const abort = () => {
|
|
57
|
+
void reader.cancel().catch(() => undefined);
|
|
58
|
+
};
|
|
59
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
60
|
+
if (signal.aborted) abort();
|
|
61
|
+
const bytes = new Uint8Array(BOOTSTRAP_RESPONSE_BYTES);
|
|
62
|
+
let length = 0;
|
|
63
|
+
try {
|
|
64
|
+
while (true) {
|
|
65
|
+
const chunk = await reader.read();
|
|
66
|
+
if (chunk.done) break;
|
|
67
|
+
if (length + chunk.value.byteLength > bytes.byteLength) {
|
|
68
|
+
void reader.cancel().catch(() => undefined);
|
|
69
|
+
throw tooLarge();
|
|
70
|
+
}
|
|
71
|
+
bytes.set(chunk.value, length);
|
|
72
|
+
length += chunk.value.byteLength;
|
|
73
|
+
}
|
|
74
|
+
} finally {
|
|
75
|
+
signal.removeEventListener("abort", abort);
|
|
76
|
+
reader.releaseLock();
|
|
77
|
+
}
|
|
78
|
+
signal.throwIfAborted();
|
|
79
|
+
try {
|
|
80
|
+
return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes.subarray(0, length)));
|
|
81
|
+
} catch {
|
|
82
|
+
throw new BootstrapError("permanent", "response invalid_json");
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
// Dependency injection keeps retry-budget tests fast and deterministic; not a public startup option.
|
|
86
|
+
type Dependencies = {
|
|
87
|
+
fetch: (url: URL, init: RequestInit) => Promise<Response>;
|
|
88
|
+
now: () => number;
|
|
89
|
+
wallNow: () => number;
|
|
90
|
+
sleep: (ms: number) => Promise<unknown>;
|
|
91
|
+
random: () => number;
|
|
92
|
+
log: (entry: { appId: string; attempt: number; errorClass: "transient" | "permanent" }) => void;
|
|
93
|
+
};
|
|
94
|
+
const defaults: Dependencies = {
|
|
95
|
+
fetch: (url, init) => fetch(url, init),
|
|
96
|
+
now: () => performance.now(),
|
|
97
|
+
wallNow: () => Date.now(),
|
|
98
|
+
sleep: (ms) => Bun.sleep(ms),
|
|
99
|
+
random: Math.random,
|
|
100
|
+
log: (entry) => console.warn("Core bootstrap attempt failed", entry),
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/** No application imports or listeners exist while this bounded startup request waits. */
|
|
104
|
+
export const bootstrapConnections = async (
|
|
105
|
+
key: AppKey,
|
|
106
|
+
origin: string | undefined,
|
|
107
|
+
dependencies: Partial<Dependencies> = {},
|
|
108
|
+
): Promise<BootstrapResponse> => {
|
|
109
|
+
const url = originUrl(origin);
|
|
110
|
+
const deps = { ...defaults, ...dependencies };
|
|
111
|
+
const deadline = deps.now() + BOOTSTRAP_BUDGET_MS;
|
|
112
|
+
let attempt = 0;
|
|
113
|
+
while (deps.now() < deadline) {
|
|
114
|
+
attempt++;
|
|
115
|
+
const controller = new AbortController();
|
|
116
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
117
|
+
let failure: BootstrapError;
|
|
118
|
+
let httpFailure: BootstrapError | undefined;
|
|
119
|
+
try {
|
|
120
|
+
const transmission = async (): Promise<BootstrapResponse> => {
|
|
121
|
+
const proof = await signBootstrapProof(key, deps.wallNow()).catch(() => {
|
|
122
|
+
throw new BootstrapError("permanent", "could not create Ed25519 startup proof");
|
|
123
|
+
});
|
|
124
|
+
controller.signal.throwIfAborted();
|
|
125
|
+
const response = await deps.fetch(url, {
|
|
126
|
+
method: "POST",
|
|
127
|
+
headers: { Authorization: `Bearer ${proof}`, "Content-Type": "application/json", Accept: "application/json" },
|
|
128
|
+
body: BOOTSTRAP_BODY,
|
|
129
|
+
redirect: "error",
|
|
130
|
+
signal: controller.signal,
|
|
131
|
+
});
|
|
132
|
+
if (response.status !== 200) {
|
|
133
|
+
// A failed transmission may already have consumed its jti.
|
|
134
|
+
httpFailure = new BootstrapError(
|
|
135
|
+
response.status === 429 || response.status === 503 ? "transient" : "permanent",
|
|
136
|
+
httpReason(response.status),
|
|
137
|
+
retryAfter(response.headers.get("retry-after"), deps.wallNow()),
|
|
138
|
+
);
|
|
139
|
+
if (response.status === 403) {
|
|
140
|
+
try {
|
|
141
|
+
// Recognize only the stable permission code; never retain or log raw error payloads.
|
|
142
|
+
const body = await readResponse(response, controller.signal);
|
|
143
|
+
if (typeof body === "object" && body !== null && "code" in body && body.code === "connections_not_permitted")
|
|
144
|
+
httpFailure = new BootstrapError("permanent", "HTTP 403: connections_not_permitted; this key cannot receive connections");
|
|
145
|
+
} catch {
|
|
146
|
+
// A broken or stalled error body cannot turn a permanent HTTP status into a retry.
|
|
147
|
+
}
|
|
148
|
+
} else void response.body?.cancel().catch(() => undefined);
|
|
149
|
+
throw httpFailure;
|
|
150
|
+
}
|
|
151
|
+
const parsed = await readResponse(response, controller.signal);
|
|
152
|
+
const checked = BootstrapResponseSchema.safeParse(parsed);
|
|
153
|
+
if (!checked.success) throw new BootstrapError("permanent", "invalid response schema");
|
|
154
|
+
const { identity } = checked.data;
|
|
155
|
+
if (identity.installationId !== key.installationId) throw new BootstrapError("permanent", "wrong installation");
|
|
156
|
+
if (identity.appId !== key.appId) throw new BootstrapError("permanent", "wrong app");
|
|
157
|
+
if (identity.keyId !== key.keyId) throw new BootstrapError("permanent", "wrong key");
|
|
158
|
+
return checked.data;
|
|
159
|
+
};
|
|
160
|
+
return await Promise.race([
|
|
161
|
+
transmission(),
|
|
162
|
+
new Promise<never>((_, reject) => {
|
|
163
|
+
timer = setTimeout(
|
|
164
|
+
() => {
|
|
165
|
+
controller.abort();
|
|
166
|
+
reject(new BootstrapError("transient", "attempt deadline exceeded"));
|
|
167
|
+
},
|
|
168
|
+
Math.min(BOOTSTRAP_ATTEMPT_MS, Math.max(0, deadline - deps.now())),
|
|
169
|
+
);
|
|
170
|
+
}),
|
|
171
|
+
]);
|
|
172
|
+
} catch (error) {
|
|
173
|
+
// Network/TLS/CA errors may embed credentials or destinations; never retain their text or cause.
|
|
174
|
+
const redirect = error instanceof TypeError && "code" in error && error.code === "UnexpectedRedirect";
|
|
175
|
+
failure =
|
|
176
|
+
httpFailure ??
|
|
177
|
+
(error instanceof BootstrapError
|
|
178
|
+
? error
|
|
179
|
+
: redirect
|
|
180
|
+
? new BootstrapError("permanent", "redirect refused; check the direct Core origin")
|
|
181
|
+
: new BootstrapError("transient", "network or TLS failure"));
|
|
182
|
+
deps.log({ appId: key.appId, attempt, errorClass: failure.kind });
|
|
183
|
+
if (failure.kind === "permanent") throw failure;
|
|
184
|
+
} finally {
|
|
185
|
+
clearTimeout(timer);
|
|
186
|
+
controller.abort();
|
|
187
|
+
}
|
|
188
|
+
// Full jitter, capped at the per-attempt deadline. Retry-After remains a minimum wait.
|
|
189
|
+
const delay = Math.max(failure.retryAfterMs, deps.random() * Math.min(BOOTSTRAP_ATTEMPT_MS, 250 * 2 ** Math.min(attempt - 1, 6)));
|
|
190
|
+
const remaining = deadline - deps.now();
|
|
191
|
+
if (delay >= remaining) break;
|
|
192
|
+
await deps.sleep(delay);
|
|
193
|
+
}
|
|
194
|
+
throw new BootstrapError("transient", "five-minute startup budget exhausted");
|
|
195
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/** Bootstrap v1 has no platform barrel or application dependencies. */
|
|
2
|
+
import { SignJWT } from "jose";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { type AppKey, importAppKeySeed } from "../app-key";
|
|
5
|
+
import type { PlatformConnectionValues } from "./connections";
|
|
6
|
+
|
|
7
|
+
export const BOOTSTRAP_PATH = "/_internal/apps/bootstrap";
|
|
8
|
+
export const BOOTSTRAP_TYPE = "cloud-app+jwt";
|
|
9
|
+
export const BOOTSTRAP_BODY = "{}";
|
|
10
|
+
export const BOOTSTRAP_RESPONSE_BYTES = 64 * 1024;
|
|
11
|
+
// Same compact-token budget as Cloud's identity protocols.
|
|
12
|
+
export const BOOTSTRAP_PROOF_BYTES = 4 * 1024;
|
|
13
|
+
export const BOOTSTRAP_BUDGET_MS = 5 * 60_000;
|
|
14
|
+
export const BOOTSTRAP_ATTEMPT_MS = 10_000;
|
|
15
|
+
export const BOOTSTRAP_PROOF_SECONDS = 30;
|
|
16
|
+
export const BOOTSTRAP_REPLAY_RETENTION_SECONDS = 5 * 60;
|
|
17
|
+
|
|
18
|
+
const nonempty = z.string().refine((value) => value.trim().length > 0);
|
|
19
|
+
export const BootstrapIdentitySchema = z.object({ installationId: z.uuid(), appId: z.string(), keyId: z.uuid() }).strict();
|
|
20
|
+
export type BootstrapIdentity = z.infer<typeof BootstrapIdentitySchema>;
|
|
21
|
+
export const BootstrapConnectionsSchema = z
|
|
22
|
+
.object({
|
|
23
|
+
database: z.object({ url: nonempty }).strict(),
|
|
24
|
+
valkey: z.object({ url: nonempty }).strict(),
|
|
25
|
+
nats: z
|
|
26
|
+
.object({
|
|
27
|
+
servers: z.array(nonempty).nonempty(),
|
|
28
|
+
namespace: nonempty,
|
|
29
|
+
replicas: z.union([z.literal(1), z.literal(3), z.literal(5)]),
|
|
30
|
+
ignoreClusterUpdates: z.boolean(),
|
|
31
|
+
creds: nonempty.optional(),
|
|
32
|
+
ca: nonempty.optional(),
|
|
33
|
+
})
|
|
34
|
+
.strict(),
|
|
35
|
+
appSecret: z.object({ current: nonempty, previous: nonempty.optional(), next: nonempty.optional() }).strict(),
|
|
36
|
+
})
|
|
37
|
+
.strict() satisfies z.ZodType<PlatformConnectionValues>;
|
|
38
|
+
export const BootstrapResponseSchema = z.object({ identity: BootstrapIdentitySchema, connections: BootstrapConnectionsSchema }).strict();
|
|
39
|
+
export type BootstrapResponse = z.infer<typeof BootstrapResponseSchema>;
|
|
40
|
+
|
|
41
|
+
export const bootstrapBodyDigest = async (body: Uint8Array<ArrayBuffer>): Promise<string> =>
|
|
42
|
+
Buffer.from(await crypto.subtle.digest("SHA-256", body)).toString("base64url");
|
|
43
|
+
|
|
44
|
+
/** Call at the HTTP transmission seam, including every retry. */
|
|
45
|
+
export const signAppProof = async (
|
|
46
|
+
key: AppKey,
|
|
47
|
+
request: { method: string; path: string; body: Uint8Array<ArrayBuffer> },
|
|
48
|
+
now = Date.now(),
|
|
49
|
+
): Promise<string> => {
|
|
50
|
+
const iat = Math.floor(now / 1000);
|
|
51
|
+
return new SignJWT({
|
|
52
|
+
htm: request.method,
|
|
53
|
+
htu: request.path,
|
|
54
|
+
bdh: await bootstrapBodyDigest(request.body),
|
|
55
|
+
})
|
|
56
|
+
.setProtectedHeader({ alg: "EdDSA", typ: BOOTSTRAP_TYPE, kid: key.keyId })
|
|
57
|
+
.setIssuer(key.appId)
|
|
58
|
+
.setSubject(key.appId)
|
|
59
|
+
.setAudience(`cloud-core:${key.installationId}`)
|
|
60
|
+
.setIssuedAt(iat)
|
|
61
|
+
.setExpirationTime(iat + BOOTSTRAP_PROOF_SECONDS)
|
|
62
|
+
.setJti(crypto.randomUUID())
|
|
63
|
+
.sign(await importAppKeySeed(key.seed));
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export const signBootstrapProof = (key: AppKey, now = Date.now()): Promise<string> =>
|
|
67
|
+
signAppProof(key, { method: "POST", path: BOOTSTRAP_PATH, body: new TextEncoder().encode(BOOTSTRAP_BODY) }, now);
|
package/src/runtime/clients.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { RedisClient, type RedisOptions, SQL } from "bun";
|
|
2
2
|
import { postgresApplicationOptions } from "../_internal/postgres-application-name";
|
|
3
|
-
import { connections
|
|
3
|
+
import { connections } from "./connections";
|
|
4
4
|
|
|
5
5
|
export type SqlPoolOptions = SQL.PostgresOrMySQLOptions;
|
|
6
6
|
|
|
@@ -28,6 +28,8 @@ const notStarted = <T>(name: string): T => {
|
|
|
28
28
|
|
|
29
29
|
export let sql: SQL = notStarted("sql");
|
|
30
30
|
export let redis: RedisClient = notStarted("redis");
|
|
31
|
+
// Whether `sql` and `redis` hold native clients; teardown must not touch the placeholders.
|
|
32
|
+
let live = false;
|
|
31
33
|
|
|
32
34
|
/** Own pool and lifecycle; defaults to the installation URL and application name. */
|
|
33
35
|
export const createSql = (options: SqlPoolOptions = {}): SQL => {
|
|
@@ -55,6 +57,7 @@ export const initializeClients = async (): Promise<void> => {
|
|
|
55
57
|
const valkey = createRedis();
|
|
56
58
|
sql = database;
|
|
57
59
|
redis = valkey;
|
|
60
|
+
live = true;
|
|
58
61
|
} catch {
|
|
59
62
|
await database.close();
|
|
60
63
|
throw new Error("Valkey client configuration is invalid; check REDIS_URL and native Redis options");
|
|
@@ -63,7 +66,8 @@ export const initializeClients = async (): Promise<void> => {
|
|
|
63
66
|
|
|
64
67
|
/** Internal test-target teardown, before a fixture selects a different database. */
|
|
65
68
|
export const closePlatformClients = async (): Promise<void> => {
|
|
66
|
-
if (
|
|
69
|
+
if (live) {
|
|
70
|
+
live = false;
|
|
67
71
|
await sql.close();
|
|
68
72
|
redis.close();
|
|
69
73
|
}
|
|
@@ -17,9 +17,18 @@ export type PlatformConnectionValues = {
|
|
|
17
17
|
export type PlatformConnectionSources = {
|
|
18
18
|
[Group in keyof PlatformConnectionValues]: { [Key in keyof PlatformConnectionValues[Group]]: ConnectionSource };
|
|
19
19
|
};
|
|
20
|
+
/** Credential-free capability metadata; absent on older processes means unknown. */
|
|
21
|
+
export type PlatformBootstrapMetadata = {
|
|
22
|
+
keyId?: string;
|
|
23
|
+
mode: "core" | "env";
|
|
24
|
+
sources: PlatformConnectionSources;
|
|
25
|
+
keyFormat: "cloud-app-key-v1";
|
|
26
|
+
protocol: 1;
|
|
27
|
+
};
|
|
20
28
|
export type PlatformConnections = PlatformConnectionValues & {
|
|
21
29
|
appId: string;
|
|
22
30
|
sources: PlatformConnectionSources;
|
|
31
|
+
bootstrap?: PlatformBootstrapMetadata;
|
|
23
32
|
};
|
|
24
33
|
|
|
25
34
|
let current: PlatformConnections | undefined;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { hkdfSync } from "node:crypto";
|
|
2
|
+
import { getProcessApplicationId } from "../_internal/process-identity";
|
|
3
|
+
import { validatePurposeName } from "../_internal/purpose-name";
|
|
4
|
+
import { connections } from "../runtime/connections";
|
|
5
|
+
|
|
6
|
+
/** Derive a purpose-specific signing key from this installation's current secret. */
|
|
7
|
+
export const appKey = (name: string): string => {
|
|
8
|
+
validatePurposeName(name);
|
|
9
|
+
const appId = getProcessApplicationId();
|
|
10
|
+
if (!appId) throw new Error("Start the application before using appKey()");
|
|
11
|
+
const secret = connections().appSecret.current;
|
|
12
|
+
if (!secret.trim()) throw new Error("APP_SECRET is required to derive application signing keys");
|
|
13
|
+
return Buffer.from(hkdfSync("sha256", secret, appId, name, 32)).toString("base64url");
|
|
14
|
+
};
|
package/src/server/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { buildMetadata } from "../_internal/build-metadata";
|
|
2
2
|
export { loadPendingApprovals } from "../api/approvals-loader";
|
|
3
3
|
export { geoClient } from "../capabilities/geo-client";
|
|
4
|
+
export { appCaller } from "../capabilities/server-invoke";
|
|
4
5
|
export { expectUserBackedActor, getUserBackedActor, isAppSessionActor, userFromActor } from "./actor";
|
|
5
6
|
export type { ApiErrorBody, ApiErrorResponse, ApiErrorStatus } from "./api";
|
|
6
7
|
export { api, respond, respondMessage } from "./api";
|
|
@@ -8,6 +9,7 @@ export type { CreateApiClientConfig } from "./api-client";
|
|
|
8
9
|
export { api as apiClient } from "./api-client";
|
|
9
10
|
export { appAssetPath } from "./app-assets";
|
|
10
11
|
export type { AppContext } from "./app-context";
|
|
12
|
+
export { appKey } from "./app-key";
|
|
11
13
|
export { clientIp, clientIpBucket } from "./client-ip";
|
|
12
14
|
export { defineHelp, defineHelpCollection, type HelpCollection, type HelpDefinition, type HelpDefinitionDocument } from "./help";
|
|
13
15
|
export { DEFAULT_LOCALE, getLocale, LOCALE_COOKIE, LOCALE_HEADER, locale, preferredLocale, resolveLocale } from "./locale";
|
package/src/server/ratelimit.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { err, fail, ok, type Result } from "@k2b/stdlib";
|
|
2
|
-
import {
|
|
2
|
+
import type { SQLQuery } from "bun";
|
|
3
3
|
import { type ManagerCandidate, removesLastManager } from "../../access/managers";
|
|
4
4
|
import { accessMessages } from "../../access/messages";
|
|
5
|
+
import { sql } from "../../runtime/clients";
|
|
5
6
|
import { recursiveGroupIdsSubquery, recursiveServiceAccountGroupIdsSubquery } from "../../services/accounts/group-sql";
|
|
6
7
|
import { toPgUuidArray } from "../../services/postgres";
|
|
7
8
|
import type { ServiceAccountKind } from "../../services/service-accounts";
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { sql } from "bun";
|
|
2
1
|
import {
|
|
3
2
|
type AccountCategory,
|
|
4
3
|
type AccountCategoryPolicy,
|
|
5
4
|
accountCategory,
|
|
6
5
|
DEFAULT_ACCOUNT_CATEGORY_POLICY,
|
|
7
6
|
} from "../contracts/account-categories";
|
|
7
|
+
import { sql } from "../runtime/clients";
|
|
8
8
|
import { decryptValue } from "./settings/crypto";
|
|
9
9
|
|
|
10
10
|
const prefix = "user.category.";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { sql } from "bun";
|
|
2
1
|
import type { User } from "../../contracts/shared";
|
|
2
|
+
import { sql } from "../../runtime/clients";
|
|
3
3
|
import { err, fail, freeipa, ok, type Result } from "../../server/services";
|
|
4
4
|
import { dates } from "../../shared";
|
|
5
5
|
import { getConfiguredExpiryDays, parseIpaAccountTransitionPolicy } from "../account-model";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { JobContext, PumpConfig, Sync } from "@k2b/sync";
|
|
2
|
-
import { sql } from "
|
|
2
|
+
import { sql } from "../../runtime/clients";
|
|
3
|
+
|
|
3
4
|
import { freeipa } from "../../server/services/freeipa";
|
|
4
5
|
import { getConfiguredExpiryDays } from "../account-model";
|
|
5
6
|
import { getFreeIpaConfig } from "../freeipa-config";
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { sql } from "bun";
|
|
2
1
|
import { accountCategory } from "../../contracts/account-categories";
|
|
3
2
|
import type { PersonAvatar } from "../../contracts/avatar";
|
|
4
3
|
import type {
|
|
@@ -14,6 +13,7 @@ import type {
|
|
|
14
13
|
UserProfile,
|
|
15
14
|
UserProvider,
|
|
16
15
|
} from "../../contracts/shared";
|
|
16
|
+
import { sql } from "../../runtime/clients";
|
|
17
17
|
import type { ServiceAccountRequestActor } from "../../server/middleware/auth";
|
|
18
18
|
import {
|
|
19
19
|
err,
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { sql } from "bun";
|
|
3
2
|
import {
|
|
4
3
|
type AvatarConfig,
|
|
5
4
|
type AvatarKind,
|
|
@@ -12,6 +11,7 @@ import {
|
|
|
12
11
|
} from "../../contracts/avatar";
|
|
13
12
|
import { MAX_AVATAR_BYTES, MAX_AVATAR_DATA_URL_LENGTH } from "../../contracts/profile";
|
|
14
13
|
import type { MutationResult } from "../../contracts/shared";
|
|
14
|
+
import { sql } from "../../runtime/clients";
|
|
15
15
|
import { AVATAR_RENDERER_REVISION, renderAvatarSvg } from "../../shared/avatar-art";
|
|
16
16
|
import { stripImageMetadata } from "../image-metadata";
|
|
17
17
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { err, fail, ok, type Result } from "@k2b/stdlib";
|
|
2
|
-
import { sql } from "bun";
|
|
3
2
|
import { z } from "zod";
|
|
4
3
|
import type { User } from "../../contracts/shared";
|
|
4
|
+
import { sql } from "../../runtime/clients";
|
|
5
5
|
import type { RequestActor } from "../../server/middleware/auth";
|
|
6
6
|
import { audit } from "../audit";
|
|
7
7
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { err, fail, ok, type PageParams, type Paginated, paginate, type Result } from "@k2b/stdlib";
|
|
2
|
-
import { sql } from "bun";
|
|
3
2
|
import type { User } from "../../contracts/shared";
|
|
3
|
+
import { sql } from "../../runtime/clients";
|
|
4
4
|
import { type AccountsActor, isAdminActor } from "./authz";
|
|
5
5
|
|
|
6
6
|
export type DuplicateEmailUser = Pick<
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { SQLQuery } from "bun";
|
|
2
2
|
import type { EntityKind, EntityListItem, UserProfile, UserProvider } from "../../contracts/shared";
|
|
3
|
+
import { sql } from "../../runtime/clients";
|
|
3
4
|
import { getFreeIpaConfig } from "../freeipa-config";
|
|
4
5
|
import { escapeLikePattern, toPgTextArray, toPgUuidArray } from "../postgres";
|
|
5
6
|
import type { ServiceAccountKind, ServiceAccountStatus } from "../service-accounts";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { sql } from "bun";
|
|
2
1
|
import type { BaseGroup, GroupMember, MutationResult, UserProvider } from "../../contracts/shared";
|
|
2
|
+
import { sql } from "../../runtime/clients";
|
|
3
3
|
import { freeipa } from "../../server/services";
|
|
4
4
|
import { getServiceIpaSession } from "../ipa/service-account";
|
|
5
5
|
import { toPgUuidArray } from "../postgres";
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { SQLQuery } from "bun";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { DEFAULT_LINUX_IDENTITY_CONFIGURATION, LinuxIdentityConfigurationSchema } from "../../contracts/posix";
|
|
4
4
|
import type { RequestActor, UserProfile, UserProvider } from "../../contracts/shared";
|
|
5
|
+
import { sql } from "../../runtime/clients";
|
|
5
6
|
import { userFromActor } from "../../server/actor";
|
|
6
7
|
import { isAccountCategoryAllowed } from "../account-category-policy";
|
|
7
8
|
import { toPgTextArray } from "../postgres";
|
|
@@ -66,9 +67,10 @@ const cursor = (after?: string): string | null => {
|
|
|
66
67
|
};
|
|
67
68
|
|
|
68
69
|
/** Server-side identity reads. Authentication and application resource authorization remain caller-owned. */
|
|
69
|
-
export const createAccountIdentityService = (
|
|
70
|
+
export const createAccountIdentityService = (injectedDb?: typeof sql, upstreamIdentity = readUpstreamIdentity) => {
|
|
71
|
+
const db = () => injectedDb ?? sql;
|
|
70
72
|
const setting = async (key: string, fallback: unknown): Promise<unknown> => {
|
|
71
|
-
const [row] = await db<{ value: string }[]>`SELECT value FROM settings.entries WHERE key = ${key}`;
|
|
73
|
+
const [row] = await db()<{ value: string }[]>`SELECT value FROM settings.entries WHERE key = ${key}`;
|
|
72
74
|
return row ? decryptValue(row.value) : fallback;
|
|
73
75
|
};
|
|
74
76
|
const availability = async (): Promise<AccountIdentityAvailability> => {
|
|
@@ -84,7 +86,7 @@ export const createAccountIdentityService = (db: typeof sql = sql, upstreamIdent
|
|
|
84
86
|
throw new AccountIdentityError("invalid_identity_configuration");
|
|
85
87
|
}
|
|
86
88
|
};
|
|
87
|
-
const userRows = (filter: SQLQuery) => db<UserRow[]>`
|
|
89
|
+
const userRows = (filter: SQLQuery) => db()<UserRow[]>`
|
|
88
90
|
SELECT u.id, u.provider, u.uid, u.profile, u.admin,
|
|
89
91
|
(u.account_expires IS NOT NULL AND u.account_expires <= now()) AS expired,
|
|
90
92
|
p.managed_by, p.uid_number, p.primary_gid_number
|
|
@@ -98,7 +100,7 @@ export const createAccountIdentityService = (db: typeof sql = sql, upstreamIdent
|
|
|
98
100
|
const [row] = await userRows(sql`u.id = ${supplied.id}::uuid`);
|
|
99
101
|
if (!row || row.expired || row.provider !== supplied.provider || row.uid !== supplied.uid || row.profile !== supplied.profile)
|
|
100
102
|
throw new AccountIdentityError("identity_unavailable");
|
|
101
|
-
if (!(await isAccountCategoryAllowed(row, db))) throw new AccountIdentityError("identity_unavailable");
|
|
103
|
+
if (!(await isAccountCategoryAllowed(row, db()))) throw new AccountIdentityError("identity_unavailable");
|
|
102
104
|
const state = await availability();
|
|
103
105
|
if (row.provider === "ipa" && !state.freeipaEnabled) throw new AccountIdentityError("identity_unavailable");
|
|
104
106
|
return { row, state, supplied };
|
|
@@ -113,14 +115,14 @@ export const createAccountIdentityService = (db: typeof sql = sql, upstreamIdent
|
|
|
113
115
|
if (!Array.isArray(configured) || !configured.every((value): value is string => typeof value === "string"))
|
|
114
116
|
throw new AccountIdentityError("invalid_identity_configuration");
|
|
115
117
|
const names = configured.length > 0 ? configured : ["admins"];
|
|
116
|
-
const [membership] = await db`SELECT 1 FROM auth.ipa_user_effective_groups
|
|
118
|
+
const [membership] = await db()`SELECT 1 FROM auth.ipa_user_effective_groups
|
|
117
119
|
WHERE user_id = ${current.row.id}::uuid AND group_name = ANY(${toPgTextArray(names)}::text[]) LIMIT 1`;
|
|
118
120
|
if (!membership) throw new AccountIdentityError("admin_required");
|
|
119
121
|
}
|
|
120
122
|
return current;
|
|
121
123
|
};
|
|
122
124
|
const groupRows = async (filter: SQLQuery) => {
|
|
123
|
-
const rows = await db<{ id: string; provider: UserProvider; name: string; gid_number: number | null; personal: boolean }[]>`
|
|
125
|
+
const rows = await db()<{ id: string; provider: UserProvider; name: string; gid_number: number | null; personal: boolean }[]>`
|
|
124
126
|
SELECT g.id, g.provider, g.name, g.gid_number,
|
|
125
127
|
EXISTS(SELECT 1 FROM auth.personal_groups p WHERE p.group_id = g.id) AS personal
|
|
126
128
|
FROM auth.groups g
|
|
@@ -189,7 +191,7 @@ export const createAccountIdentityService = (db: typeof sql = sql, upstreamIdent
|
|
|
189
191
|
eligible: current.profile === "user",
|
|
190
192
|
};
|
|
191
193
|
}
|
|
192
|
-
const [ownership] = await db<{ personal: boolean }[]>`SELECT EXISTS (
|
|
194
|
+
const [ownership] = await db()<{ personal: boolean }[]>`SELECT EXISTS (
|
|
193
195
|
SELECT 1 FROM auth.personal_groups p
|
|
194
196
|
WHERE (${input.identityId ?? null}::uuid IS NOT NULL AND p.group_id = ${input.identityId ?? null}::uuid)
|
|
195
197
|
OR (${input.identityId ?? null}::uuid IS NULL AND p.group_name = ${input.name})
|
|
@@ -238,10 +240,10 @@ export const createAccountIdentityService = (db: typeof sql = sql, upstreamIdent
|
|
|
238
240
|
const after = cursor(input.after);
|
|
239
241
|
const state = await availability();
|
|
240
242
|
const [localAllowed, ipaAllowed] = await Promise.all([
|
|
241
|
-
isAccountCategoryAllowed({ provider: "local", profile: "user" }, db),
|
|
242
|
-
isAccountCategoryAllowed({ provider: "ipa", profile: "user" }, db),
|
|
243
|
+
isAccountCategoryAllowed({ provider: "local", profile: "user" }, db()),
|
|
244
|
+
isAccountCategoryAllowed({ provider: "ipa", profile: "user" }, db()),
|
|
243
245
|
]);
|
|
244
|
-
const rows = await db<
|
|
246
|
+
const rows = await db()<
|
|
245
247
|
{
|
|
246
248
|
id: string;
|
|
247
249
|
provider: UserProvider;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { TransactionSQL } from "bun";
|
|
2
2
|
import type { BaseGroup, GroupMember, MutationResult, UserProvider } from "../../contracts/shared";
|
|
3
|
+
import { sql } from "../../runtime/clients";
|
|
3
4
|
import { escapeLikePattern, isUniqueViolation } from "../postgres";
|
|
4
5
|
import { buildBaseGroup, personalOwnerJoin } from "./base-group";
|
|
5
6
|
import { recursiveServiceAccountGroupIdsSubquery } from "./group-sql";
|