Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/api/src/lib/agent-channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export const AGENT_CHANNEL_AUTONOMOUS_REQUIRED_RUNTIME_CAPABILITIES = [
'accepted_event_rehydration_v1',
] as const;
export type AgentChannelAdapterMode = 'supervised_runtime' | 'autonomous_platform';
export const DEFT_RELEASE_VERSION = process.env.DEFT_RELEASE_VERSION || '0.3.0-preview.138';
export const DEFT_RELEASE_VERSION = process.env.DEFT_RELEASE_VERSION || '0.3.0-preview.139';
export const DEFT_BUILD_COMMIT = process.env.DEFT_BUILD_COMMIT || process.env.VCS_REF || 'unknown';
export const DEFT_SCHEMA_HEAD = '0.3.0-preview.131';
export const AGENT_CHANNEL_DEFAULT_LEASE_MS = 120_000;
Expand Down
4 changes: 2 additions & 2 deletions apps/api/src/lib/app-attachment-sync-reviewed.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { digestResourceSyncDescriptorV2 } from '@deft/app-kit/core';
import { digestAppAuthorityDescriptor } from './app-authority-descriptor.js';
import type { AppRunTransaction } from './app-run-repository.js';
import { attachmentStale, loadReviewedAttachmentApp } from './app-attachment-authority.js';

Expand All @@ -9,6 +9,6 @@ export async function loadReviewedAttachmentSyncDescriptor(tx: AppRunTransaction
: reviewed.manifest.sync_descriptors.find(item => item.key === resourceKey);
if (!descriptor) throw attachmentStale();
return { installation: reviewed.installation, version: reviewed.version, grant: reviewed.grant, manifest: reviewed.manifest,
descriptor, descriptor_digest: await digestResourceSyncDescriptorV2(descriptor),
descriptor, descriptor_digest: await digestAppAuthorityDescriptor(descriptor, { org_id: orgId, app_version_id: reviewed.version.id }),
descriptors: reviewed.manifest.sync_descriptors };
}
21 changes: 21 additions & 0 deletions apps/api/src/lib/app-authority-descriptor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { digestResourceSyncDescriptorV2 } from '@deft/app-kit/core';
import { createAsyncAppAuthorityContentCache, type AppAuthorityContentScope } from './app-authority-content-cache.js';

export const APP_AUTHORITY_DESCRIPTOR_CACHE_ENTRIES = 32;
export const APP_AUTHORITY_DESCRIPTOR_CACHE_KEY_BYTES = 128 * 1024;

/** Pure Kit validation/digest reuse over complete freshly read descriptor JSON.
* IDs or advertised digests never substitute for content. Current DB, owner,
* admission, policy, epoch, configuration and deadline checks stay in callers.
* The Kit digester parses/canonicalizes/encodes synchronously before its first
* await, so it retains no caller JSON while WebCrypto completes. */
export function createAppAuthorityDescriptorDigester(digester = digestResourceSyncDescriptorV2) {
const cache = createAsyncAppAuthorityContentCache<Awaited<ReturnType<typeof digester>>>({
maxEntries: APP_AUTHORITY_DESCRIPTOR_CACHE_ENTRIES, maxBytes: 64 * 1024,
maxContentBytes: APP_AUTHORITY_DESCRIPTOR_CACHE_KEY_BYTES,
});
return (raw: unknown, scope: AppAuthorityContentScope): ReturnType<typeof digester> =>
cache(scope, raw, () => digester(raw));
}

export const digestAppAuthorityDescriptor = createAppAuthorityDescriptorDigester();
4 changes: 2 additions & 2 deletions apps/api/src/lib/app-experience-exposure-db.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { CoreDatabaseTransaction } from './database-transaction.js';
import { drizzle } from 'drizzle-orm/node-postgres';
import pg from 'pg';
import * as schema from '@deft/db/schema';
Expand All @@ -15,8 +16,7 @@ export const APP_EXPERIENCE_EXPOSURE_DB_LIMITS = Object.freeze({
confirmation_operation_ms: 15_000,
});

type ExposureDatabase = ReturnType<typeof drizzle<typeof schema>>;
export type ExperienceExposureTransaction = Parameters<Parameters<ExposureDatabase['transaction']>[0]>[0];
export type ExperienceExposureTransaction = CoreDatabaseTransaction;

/** Separate capacity and server-enforced SQL limits for Experience exposure.
* Cancellation waits for the bounded active statement and ROLLBACK; no rejected
Expand Down
4 changes: 2 additions & 2 deletions apps/api/src/lib/app-owner-connection-authority.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { connectionProfileDigest, getAppConnectionSessionRegistry, type AppConne
import { isAppOwnerConnectionsEnabled } from './env.js';
import { AppError } from './app-errors.js';
import { runtimeActionDescriptors } from './app-runtime-review.js';
import { digestResourceSyncDescriptorV2 } from '@deft/app-kit/core';
import { digestAppAuthorityDescriptor } from './app-authority-descriptor.js';
import { parseAppAuthorityManifest } from './app-authority-manifest.js';

type LockedReviewedApp = Pick<Awaited<ReturnType<typeof loadReviewedAttachmentApp>>, 'installation' | 'version' | 'grant'>
Expand Down Expand Up @@ -118,7 +118,7 @@ export async function loadOwnerConnectionAdmission(tx: AppRunTransaction, orgId:
const actions = runtimeActionDescriptors(reviewed.manifest).filter(item => policy.action_keys.includes(item.action_key))
.map(item => ({ action_key: item.action_key, contract_digest: item.contract_digest }))
.sort((a,b) => a.action_key < b.action_key ? -1 : a.action_key > b.action_key ? 1 : 0);
if (!descriptor || snapshot.resource_descriptor_digest !== await digestResourceSyncDescriptorV2(descriptor)
if (!descriptor || snapshot.resource_descriptor_digest !== await digestAppAuthorityDescriptor(descriptor, { org_id: orgId, app_version_id: reviewed.version.id })
|| digestAppGrantValue(snapshot.actions) !== digestAppGrantValue(actions)) throw ownerConnectionDenied();
// The registry stays transaction-local and never enters a serializable
// admission/snapshot result. Publish it only after every current check passed.
Expand Down
4 changes: 2 additions & 2 deletions apps/api/src/lib/app-public-budgets.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import type { CoreDatabaseTransaction } from './database-transaction.js';
import { sql, eq, and } from 'drizzle-orm';
import { PublicBudgetPolicySchema } from '@deft/app-kit/core';
import { APP_RUN_TERMINAL_STATES } from '@deft/shared';
import { appCanonicalClaims, appPublicEndpoints } from '@deft/db/schema';
import type { db } from './db.js';

type Transaction = Parameters<Parameters<typeof db.transaction>[0]>[0];
type Transaction = CoreDatabaseTransaction;
type Endpoint = typeof appPublicEndpoints.$inferSelect;
export const PUBLIC_APP_BUDGET_CEILINGS = Object.freeze({ max_pending: 25, max_confirmed_per_utc_day: 100 });
export class PublicBudgetExceededError extends Error {}
Expand Down
3 changes: 2 additions & 1 deletion apps/api/src/lib/app-resource-sync-management.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { CoreDatabaseTransaction } from './database-transaction.js';
import {appConsentDeadline} from '@deft/app-kit/core';
import { digestResourceSyncDescriptorV2 } from '@deft/app-kit/core';
import { assertAttachmentBrokerEnabled, attachmentFinalAuthorityIsCurrent } from './app-attachment-authority.js';
Expand Down Expand Up @@ -37,7 +38,7 @@ import { APP_RESOURCE_SYNC_HOST_POLICY, APP_RESOURCE_SYNC_SESSION_MS, APP_RESOUR
assertResourceSyncConsentWindow, hashAppResourceSyncToken,
type AppResourceSyncConsentRequest } from './app-resource-sync-policy.js';

type Tx = Parameters<Parameters<typeof db.transaction>[0]>[0];
type Tx = CoreDatabaseTransaction;
export type ResourceSyncManagementGuard = (tx: Tx) => Promise<void>;

async function managementTransaction<T>(guard: ResourceSyncManagementGuard | undefined,
Expand Down
3 changes: 2 additions & 1 deletion apps/api/src/lib/app-resource-sync-status.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { CoreDatabaseTransaction } from './database-transaction.js';
import { and, asc, desc, eq, gt, sql } from 'drizzle-orm';
import { z } from 'zod';
import { appResourceBindings, appRuntimeRegistrations, appSyncCheckpoints,
Expand All @@ -9,7 +10,7 @@ import { assertResourceSyncManager } from './app-resource-sync-web-authority.js'
import type { ResourceSyncManagementGuard } from './app-resource-sync-management.js';
import { finishAttachmentSyncMetadata, type AttachmentSyncReadMode } from './app-resource-sync-read-mode.js';

type Tx = Parameters<Parameters<typeof db.transaction>[0]>[0];
type Tx = CoreDatabaseTransaction;
const Id = z.string().uuid();
export const ResourceSyncListQuery = z.strictObject({
after: Id.optional(), limit: z.coerce.number().int().min(1).max(50).default(20),
Expand Down
6 changes: 4 additions & 2 deletions apps/api/src/lib/app-run-bounded-db.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { drizzle } from 'drizzle-orm/node-postgres';
import pg from 'pg';
import * as schema from '@deft/db/schema';
import type { AppRunTransaction } from './app-run-repository.js';

export const APP_RUN_TRANSACTION_LIMITS = Object.freeze({ budget_ms: 10_000,
Expand Down Expand Up @@ -59,7 +58,10 @@ export function createBoundedAppRunDatabase(connectionString: string, options: {
return result;
};
} });
return await drizzle(guarded, { schema }).transaction(work);
// Core table queries carry their own columns and codecs. Building the
// unused relational schema here would re-extract every table and index
// for each fresh bounded transaction, including each heartbeat guard.
return await drizzle(guarded).transaction(work);
} finally {
clearTimeout(timer); signal.removeEventListener('abort', discard);
if (!settled && !released) { try { await client.query('ROLLBACK'); } catch { broken = true; } }
Expand Down
3 changes: 2 additions & 1 deletion apps/api/src/lib/app-run-repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ import type {
AppRunSubmission,
} from '@deft/shared';
import { db } from './db.js';
import type { CoreDatabaseTransaction } from './database-transaction.js';

export type AppRunTransaction = Parameters<Parameters<typeof db.transaction>[0]>[0];
export type AppRunTransaction = CoreDatabaseTransaction;

export const safeRunSelection = {
id: appRuns.id,
Expand Down
3 changes: 2 additions & 1 deletion apps/api/src/lib/app-runtime-review.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { CoreDatabaseTransaction } from './database-transaction.js';
import { randomUUID } from 'node:crypto';
import { and, desc, eq, inArray, sql } from 'drizzle-orm';
import { z } from 'zod';
Expand All @@ -23,7 +24,7 @@ export const RuntimeAppReviewRequestSchema = z.strictObject({
export const RuntimeAppActivateRequestSchema = RuntimeAppReviewRequestSchema.extend({
expected_review_digest: AppDigestSchema, accept_host_policy: z.literal(true),
});
type ReviewTransaction = Parameters<Parameters<typeof db.transaction>[0]>[0];
type ReviewTransaction = CoreDatabaseTransaction;
export type RuntimeAppReviewOptions = Readonly<{
guard?: (tx: ReviewTransaction) => Promise<void>;
assertAdmission?: (manifest: RuntimeAppManifest | DeftAppManifestV5) => void;
Expand Down
3 changes: 2 additions & 1 deletion apps/api/src/lib/app-service.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { CoreDatabaseTransaction } from './database-transaction.js';
import { randomUUID } from 'node:crypto';
import { and, asc, desc, eq, inArray, sql } from 'drizzle-orm';
import {
Expand Down Expand Up @@ -216,7 +217,7 @@ export async function inspectAppPackageJson(value: string): Promise<InspectedApp
export async function stageAppPackage(
actor: ModuleActor,
packageJson: string,
options: { guard?: (tx: Parameters<Parameters<typeof db.transaction>[0]>[0]) => Promise<void>;
options: { guard?: (tx: CoreDatabaseTransaction) => Promise<void>;
attachmentStage?: boolean; attachmentComposition?: boolean } = {},
): Promise<AppInstallationView> {
assertHumanManager(actor);
Expand Down
3 changes: 2 additions & 1 deletion apps/api/src/lib/app-upgrade-trust-continuity.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { CoreDatabaseTransaction } from './database-transaction.js';
import { randomUUID } from 'node:crypto';
import { isDeepStrictEqual } from 'node:util';
import { and, eq, sql, inArray } from 'drizzle-orm';
Expand Down Expand Up @@ -25,7 +26,7 @@ import type { WebAuthorityGuard } from './app-resource-sync-web-authority.js';
import { REATTACH_PHASE_MS } from './app-connection-reattachment-deadline.js';
import { assertCurrentUpgradeRowPins } from './app-upgrade-row-pins.js';

export type UpgradeTx = Parameters<Parameters<typeof db.transaction>[0]>[0];
export type UpgradeTx = CoreDatabaseTransaction;
const denied = () => new AppError('Automatic upgrade continuity requires review', 'APP_REVIEW_REQUIRED', 409);
export const UPGRADE_PROOF_ACTION = 'app.upgrade_continuity.authorized';
export const UPGRADE_ALIAS_ACTION = 'app.upgrade_continuity.registration';
Expand Down
3 changes: 2 additions & 1 deletion apps/api/src/lib/app-upgrade-trust-policy.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { CoreDatabaseTransaction } from './database-transaction.js';
import { and, eq, sql } from 'drizzle-orm';
import { z } from 'zod';
import { appInstallations, auditLog } from '@deft/db/schema';
Expand All @@ -8,7 +9,7 @@ import { assertCurrentModuleManagerWithExecutor } from './module-service.js';
import type { WebAuthorityGuard } from './app-resource-sync-web-authority.js';
import { diffUpgradeAuthority, type UpgradeAuthorityDiff } from './app-upgrade-manifest-diff.js';

type Tx = Parameters<Parameters<typeof db.transaction>[0]>[0];
type Tx = CoreDatabaseTransaction;
const TRUST_ACTION = 'app.upgrade_continuity_trust_changed';
const TrustSchema = z.strictObject({ trusted: z.boolean(), revision: z.number().int().nonnegative().max(2147483647) });
export const UpgradeTrustUpdateSchema = TrustSchema.safeExtend({ expected_revision: z.number().int().nonnegative().max(2147483646) }).omit({ revision: true });
Expand Down
11 changes: 11 additions & 0 deletions apps/api/src/lib/database-transaction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { NodePgDatabase } from 'drizzle-orm/node-postgres';

type Transaction = Parameters<Parameters<NodePgDatabase['transaction']>[0]>[0];

/** Table queries carry their own columns and codecs. Shared helpers need no
* relational schema, client handles or driver internals. Nested callbacks keep
* the same contract even when the caller uses a schema-less bounded client. */
export type CoreDatabaseTransaction = Pick<Transaction,
'select' | 'selectDistinct' | 'selectDistinctOn' | 'insert' | 'update' | 'delete' | 'execute' | 'rollback'> & {
transaction<T>(work: (tx: CoreDatabaseTransaction) => Promise<T>): Promise<T>;
};
3 changes: 2 additions & 1 deletion apps/api/src/lib/native-calendar.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import type { CoreDatabaseTransaction } from './database-transaction.js';
import { and, eq } from 'drizzle-orm';
import { events } from '@deft/db/schema';
import { db } from './db.js';

type Transaction = Parameters<Parameters<typeof db.transaction>[0]>[0];
type Transaction = CoreDatabaseTransaction;
type CalendarInput = {
title: string; start: string; end: string; description?: string; location?: string;
attendees?: { email: string; displayName?: string; name?: string }[];
Expand Down
3 changes: 2 additions & 1 deletion apps/api/src/lib/native-create.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import type { CoreDatabaseTransaction } from './database-transaction.js';
import { createHash } from 'node:crypto';
import { eq, sql } from 'drizzle-orm';
import { z } from 'zod';
import { nativeCreateRequests } from '@deft/db/schema';
import { db } from './db.js';

type Transaction = Parameters<Parameters<typeof db.transaction>[0]>[0];
type Transaction = CoreDatabaseTransaction;
const keySchema = z.string().min(1).max(128).regex(/^[A-Za-z0-9._:-]+$/);

export class NativeCreateError extends Error {
Expand Down
5 changes: 3 additions & 2 deletions apps/api/src/lib/native-mentions.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { CoreDatabaseTransaction } from './database-transaction.js';
import { DEFTY_SYSTEM_EMPLOYEE_SLUG, DEFTY_SYSTEM_RUNTIME_KIND } from './defty-identity.js';
import { createHash } from 'node:crypto';
import { and, desc, eq, sql } from 'drizzle-orm';
Expand Down Expand Up @@ -101,7 +102,7 @@ export async function loadNativeSource(
return visible.length && await employeeBoundary(ctx, row, executor) ? row : null;
}

async function reconcileWithin(executor: Parameters<Parameters<typeof db.transaction>[0]>[0], orgId: string, source: NativeMentionSource) {
async function reconcileWithin(executor: CoreDatabaseTransaction, orgId: string, source: NativeMentionSource) {
const current = await loadNativeSource({ orgId }, source, executor, true);
if (!current) {
await executor.update(nativeReferenceStates).set({ current_refs: [], is_deleted: true, updated_at: new Date() })
Expand Down Expand Up @@ -194,7 +195,7 @@ export async function handleNativeMentionReconciliation(data: { orgId: string; s

/** Called only at authenticated human Send/Post boundaries, in the content transaction. */
export async function enqueueNativeMentionPublication(
executor: Parameters<Parameters<typeof db.transaction>[0]>[0],
executor: CoreDatabaseTransaction,
ctx: NativeMentionContext, source: NativeMentionSource, content: string,
) {
if (!nativeMentionsEnabled()) return;
Expand Down
19 changes: 18 additions & 1 deletion apps/api/test/app-attachment-authority-payload.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ function fixture() {
return returned;
}).then(resolve, reject) }; return chain;
} };
return { org, installationId, rows, version, requested, grant, calls,
return { org, installationId, rows, version, requested, grant, calls, tx,
run: () => loadReviewedAttachmentApp(tx as never, org, installationId) };
}

Expand Down Expand Up @@ -148,3 +148,20 @@ test('resource snapshot serialization uses only the current version ID and packa
const changed = buildResourceSyncAuthorizationSnapshot({ ...facts, version: guarded } as never);
assert.notDeepEqual(changed, full, 'the reviewed package digest remains part of the authority pin');
});

test('warm descriptor digest reuse still reloads current locked App/grant bytes and rejects drift', async () => {
const { loadReviewedAttachmentSyncDescriptor } = await import('../src/lib/app-attachment-sync-reviewed.js');
const f = fixture(), read = () => loadReviewedAttachmentSyncDescriptor(f.tx as never, f.org, f.installationId);
const first = await read(), before = f.calls.length, digest = first.descriptor_digest;
first.descriptor.attachments.max_attachment_bytes--;
const next = await read();
assert.equal(next.descriptor_digest, digest, 'caller mutation cannot alter a retained digest or current descriptor');
assert.equal(f.calls.length - before, 2, 'warm digest reuse still reads locked installation and exact version/grants');
assert.deepEqual(f.calls.slice(-2).map(call => [call.table, call.share]), [['app_installations', true], ['app_versions', true]]);
f.version.manifest.sync_descriptors[0].attachments.max_attachment_bytes--;
await assert.rejects(read(), 'actual descriptor drift under unchanged advertised manifest/grant digests fails current reconstruction');
const disabled = fixture();
await loadReviewedAttachmentSyncDescriptor(disabled.tx as never, disabled.org, disabled.installationId);
disabled.rows.app_installations![0]!.state = 'disabled';
await assert.rejects(loadReviewedAttachmentSyncDescriptor(disabled.tx as never, disabled.org, disabled.installationId));
});
Loading
Loading