@agent-native/core 0.77.6 → 0.77.8
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/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +29 -0
- package/corpus/core/package.json +3 -3
- package/corpus/core/src/agent/production-agent.ts +18 -0
- package/corpus/core/src/agent/thread-data-builder.ts +21 -2
- package/corpus/core/src/cli/create.ts +35 -1
- package/corpus/core/src/cli/skills.ts +8 -0
- package/corpus/core/src/client/AgentPanel.tsx +57 -25
- package/corpus/core/src/client/MultiTabAssistantChat.tsx +31 -1
- package/corpus/core/src/client/analytics.ts +24 -2
- package/corpus/core/src/client/use-agent-engine-configured.ts +29 -15
- package/corpus/core/src/client/use-chat-threads.ts +104 -55
- package/corpus/core/src/db/ddl-guard.ts +190 -0
- package/corpus/core/src/secrets/storage.ts +36 -2
- package/corpus/core/src/server/analytics.ts +27 -3
- package/corpus/core/src/settings/store.ts +40 -6
- package/corpus/core/src/sharing/access.ts +35 -13
- package/corpus/core/src/sharing/actions/share-resource.ts +32 -9
- package/corpus/core/src/sharing/actions/unshare-resource.ts +25 -2
- package/corpus/core/src/sharing/registry.ts +9 -0
- package/corpus/core/src/vite/client.ts +3 -0
- package/corpus/templates/analytics/actions/data-source-status.ts +25 -14
- package/corpus/templates/analytics/app/components/layout/NewAnalysisDialog.tsx +2 -2
- package/corpus/templates/analytics/app/lib/data-source-status.ts +47 -6
- package/corpus/templates/analytics/app/lib/data-sources.ts +14 -3
- package/corpus/templates/analytics/app/pages/DataSources.tsx +24 -5
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/PanelEditorDialog.tsx +15 -43
- package/corpus/templates/analytics/changelog/2026-06-25-fixed-hubspot-data-source-status-so-private-app-tokens-legac.md +6 -0
- package/corpus/templates/analytics/netlify.toml +1 -0
- package/corpus/templates/analytics/server/lib/credential-keys.ts +3 -1
- package/corpus/templates/analytics/server/routes/api/test-connection.post.ts +12 -3
- package/corpus/templates/assets/app/components/layout/Sidebar.tsx +5 -2
- package/corpus/templates/assets/changelog/2026-06-25-create-now-returns-from-agent-settings-to-the-asset-chat-eve.md +6 -0
- package/corpus/templates/assets/netlify.toml +1 -0
- package/corpus/templates/brain/netlify.toml +1 -0
- package/corpus/templates/calendar/app/i18n-data.ts +12 -0
- package/corpus/templates/calendar/app/lib/clipboard.ts +58 -0
- package/corpus/templates/calendar/app/pages/AvailabilitySettings.tsx +15 -9
- package/corpus/templates/calendar/app/pages/BookingLinksPage.tsx +6 -2
- package/corpus/templates/calendar/changelog/2026-06-25-booking-link-copy-buttons-no-longer-fail-when-clipboard-perm.md +6 -0
- package/corpus/templates/calendar/netlify.toml +1 -0
- package/corpus/templates/chat/netlify.toml +1 -0
- package/corpus/templates/clips/AGENTS.md +9 -7
- package/corpus/templates/clips/actions/add-vocabulary-term.ts +2 -2
- package/corpus/templates/clips/actions/archive-recording.ts +5 -2
- package/corpus/templates/clips/actions/clear-edits.ts +5 -2
- package/corpus/templates/clips/actions/create-folder.ts +7 -2
- package/corpus/templates/clips/actions/delete-folder.ts +7 -4
- package/corpus/templates/clips/actions/delete-recording-permanent.ts +5 -2
- package/corpus/templates/clips/actions/finalize-recording.ts +5 -2
- package/corpus/templates/clips/actions/import-loom-recording.ts +2 -1
- package/corpus/templates/clips/actions/list-notifications.ts +9 -5
- package/corpus/templates/clips/actions/list-organization-state.ts +2 -1
- package/corpus/templates/clips/actions/list-recordings.ts +4 -1
- package/corpus/templates/clips/actions/move-recording.ts +5 -1
- package/corpus/templates/clips/actions/rename-folder.ts +6 -3
- package/corpus/templates/clips/actions/request-transcript.ts +5 -2
- package/corpus/templates/clips/actions/restore-recording.ts +5 -2
- package/corpus/templates/clips/actions/save-browser-diagnostics.ts +21 -8
- package/corpus/templates/clips/actions/set-chapters.ts +5 -2
- package/corpus/templates/clips/actions/set-thumbnail.ts +5 -2
- package/corpus/templates/clips/actions/split-recording.ts +5 -2
- package/corpus/templates/clips/actions/stitch-recordings.ts +6 -2
- package/corpus/templates/clips/actions/trash-recording.ts +5 -2
- package/corpus/templates/clips/actions/undo-edit.ts +5 -2
- package/corpus/templates/clips/actions/view-screen.ts +2 -1
- package/corpus/templates/clips/app/components/library/recording-card.tsx +24 -7
- package/corpus/templates/clips/app/lib/browser-diagnostics-capture.ts +5 -1
- package/corpus/templates/clips/changelog/2026-06-25-chrome-extension-recordings-now-upload-large-capture-bursts.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-25-console-diagnostics-keep-more-useful-non-secret-details-when.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-25-fixed-owner-recognition-so-clips-created-from-desktop-and-we.md +6 -0
- package/corpus/templates/clips/chrome-extension/package.json +3 -0
- package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -1
- package/corpus/templates/clips/chrome-extension/src/background.ts +85 -9
- package/corpus/templates/clips/chrome-extension/src/content-script.ts +55 -0
- package/corpus/templates/clips/chrome-extension/src/offscreen.ts +79 -3
- package/corpus/templates/clips/chrome-extension/src/options.ts +10 -0
- package/corpus/templates/clips/chrome-extension/src/overlay.ts +6 -0
- package/corpus/templates/clips/chrome-extension/src/permission.ts +8 -1
- package/corpus/templates/clips/chrome-extension/src/popup.ts +65 -39
- package/corpus/templates/clips/chrome-extension/src/sentry.ts +206 -0
- package/corpus/templates/clips/chrome-extension/vite.config.ts +47 -0
- package/corpus/templates/clips/netlify.toml +1 -0
- package/corpus/templates/clips/server/db/index.ts +1 -0
- package/corpus/templates/clips/server/lib/google-calendar-oauth.ts +35 -23
- package/corpus/templates/clips/server/lib/public-agent-context.ts +8 -1
- package/corpus/templates/clips/server/lib/recordings.ts +18 -2
- package/corpus/templates/clips/server/lib/slack-oauth.ts +9 -6
- package/corpus/templates/clips/server/plugins/onboarding-calendar.ts +6 -2
- package/corpus/templates/clips/server/routes/api/public-recording.get.ts +2 -2
- package/corpus/templates/clips/server/routes/api/recordings/[recordingId]/thumbnail.post.ts +5 -2
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/abort.post.ts +5 -2
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +5 -2
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/reset-chunks.post.ts +5 -2
- package/corpus/templates/clips/shared/browser-diagnostics.ts +14 -4
- package/corpus/templates/content/netlify.toml +1 -0
- package/corpus/templates/design/netlify.toml +1 -0
- package/corpus/templates/dispatch/netlify.toml +1 -0
- package/corpus/templates/forms/netlify.toml +1 -0
- package/corpus/templates/macros/netlify.toml +1 -0
- package/corpus/templates/mail/app/components/email/EmailList.tsx +7 -3
- package/corpus/templates/mail/app/components/email/EmailThread.tsx +4 -1
- package/corpus/templates/mail/app/components/layout/AppLayout.tsx +10 -2
- package/corpus/templates/mail/app/hooks/use-compose-state.ts +15 -5
- package/corpus/templates/mail/app/hooks/use-google-auth.ts +47 -9
- package/corpus/templates/mail/changelog/2026-06-25-account-avatars-stay-stable-while-mail-refreshes-google-acco.md +6 -0
- package/corpus/templates/mail/changelog/2026-06-25-mail-background-draft-saves-and-thread-prefetches-no-longer.md +6 -0
- package/corpus/templates/mail/netlify.toml +1 -0
- package/corpus/templates/plan/.agents/skills/visual-plan/references/canvas.md +8 -0
- package/corpus/templates/plan/netlify.toml +1 -0
- package/corpus/templates/slides/netlify.toml +1 -0
- package/corpus/templates/videos/netlify.toml +1 -0
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +18 -0
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/agent/thread-data-builder.d.ts.map +1 -1
- package/dist/agent/thread-data-builder.js +23 -2
- package/dist/agent/thread-data-builder.js.map +1 -1
- package/dist/cli/create.d.ts.map +1 -1
- package/dist/cli/create.js +28 -1
- package/dist/cli/create.js.map +1 -1
- package/dist/cli/skills.d.ts +1 -1
- package/dist/cli/skills.d.ts.map +1 -1
- package/dist/cli/skills.js +8 -0
- package/dist/cli/skills.js.map +1 -1
- package/dist/client/AgentPanel.d.ts +8 -2
- package/dist/client/AgentPanel.d.ts.map +1 -1
- package/dist/client/AgentPanel.js +29 -15
- package/dist/client/AgentPanel.js.map +1 -1
- package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
- package/dist/client/MultiTabAssistantChat.js +4 -4
- package/dist/client/MultiTabAssistantChat.js.map +1 -1
- package/dist/client/analytics.d.ts.map +1 -1
- package/dist/client/analytics.js +22 -2
- package/dist/client/analytics.js.map +1 -1
- package/dist/client/use-agent-engine-configured.d.ts.map +1 -1
- package/dist/client/use-agent-engine-configured.js +23 -13
- package/dist/client/use-agent-engine-configured.js.map +1 -1
- package/dist/client/use-chat-threads.d.ts +4 -0
- package/dist/client/use-chat-threads.d.ts.map +1 -1
- package/dist/client/use-chat-threads.js +49 -4
- package/dist/client/use-chat-threads.js.map +1 -1
- package/dist/collab/routes.d.ts +2 -2
- package/dist/db/ddl-guard.d.ts +88 -0
- package/dist/db/ddl-guard.d.ts.map +1 -0
- package/dist/db/ddl-guard.js +180 -0
- package/dist/db/ddl-guard.js.map +1 -0
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +5 -5
- package/dist/progress/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +3 -3
- package/dist/secrets/storage.d.ts.map +1 -1
- package/dist/secrets/storage.js +26 -2
- package/dist/secrets/storage.js.map +1 -1
- package/dist/server/agent-engine-api-key-route.d.ts +2 -2
- package/dist/server/analytics.d.ts +4 -0
- package/dist/server/analytics.d.ts.map +1 -1
- package/dist/server/analytics.js +19 -3
- package/dist/server/analytics.js.map +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/settings/store.d.ts.map +1 -1
- package/dist/settings/store.js +32 -6
- package/dist/settings/store.js.map +1 -1
- package/dist/sharing/access.d.ts.map +1 -1
- package/dist/sharing/access.js +26 -12
- package/dist/sharing/access.js.map +1 -1
- package/dist/sharing/actions/share-resource.js +20 -9
- package/dist/sharing/actions/share-resource.js.map +1 -1
- package/dist/sharing/actions/unshare-resource.js +13 -2
- package/dist/sharing/actions/unshare-resource.js.map +1 -1
- package/dist/sharing/registry.d.ts +9 -0
- package/dist/sharing/registry.d.ts.map +1 -1
- package/dist/sharing/registry.js.map +1 -1
- package/dist/vite/client.d.ts.map +1 -1
- package/dist/vite/client.js +1 -0
- package/dist/vite/client.js.map +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Guards for on-demand `ensureTable()` DDL so the common already-migrated path
|
|
3
|
+
* takes NO `ACCESS EXCLUSIVE` lock on Postgres.
|
|
4
|
+
*
|
|
5
|
+
* Lives in its own module (like `./widen-columns.js`) so stores can import it
|
|
6
|
+
* without every `vi.mock("../db/client.js")` test needing to stub it: the
|
|
7
|
+
* helpers resolve `isPostgres()` / `getDbExec()` through `client.js`, so a test
|
|
8
|
+
* that mocks the client to SQLite (`isPostgres: () => false`) makes the
|
|
9
|
+
* Postgres-only existence checks no-ops automatically.
|
|
10
|
+
*
|
|
11
|
+
* ## Why
|
|
12
|
+
*
|
|
13
|
+
* `ensureTable()` runs once per process on first DB touch. In a long-lived Node
|
|
14
|
+
* server the cost is paid once and is invisible. In a Netlify `-background`
|
|
15
|
+
* function the process is fresh, so this is the FIRST touch — and the
|
|
16
|
+
* `CREATE TABLE`/`ALTER TABLE ... ADD COLUMN` DDL it issues takes an
|
|
17
|
+
* `ACCESS EXCLUSIVE` lock on the shared Neon Postgres database. Behind a
|
|
18
|
+
* concurrent connection that already holds a conflicting lock, that DDL can
|
|
19
|
+
* block ~indefinitely (observed >16s hangs in the bg worker vs ~1s inline).
|
|
20
|
+
*
|
|
21
|
+
* The tables/columns are essentially always already present in production, so
|
|
22
|
+
* the DDL is redundant in the hot path. These helpers let a store cheaply check
|
|
23
|
+
* `information_schema` first and only issue DDL when something is actually
|
|
24
|
+
* missing — and when DDL must run, wrap it in a short `lock_timeout` so a
|
|
25
|
+
* contended lock fails fast instead of hanging.
|
|
26
|
+
*
|
|
27
|
+
* All of this is Postgres-only behaviour gated on `isPostgres()`. On SQLite
|
|
28
|
+
* (local dev) there is no such lock problem, so callers keep their existing
|
|
29
|
+
* behaviour there.
|
|
30
|
+
*/
|
|
31
|
+
import { isPostgres, getDbExec } from "./client.js";
|
|
32
|
+
const PLAIN_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
33
|
+
/**
|
|
34
|
+
* True when running against Postgres AND the given table already exists in the
|
|
35
|
+
* `public` schema. Returns `false` on SQLite (callers gate their own behaviour
|
|
36
|
+
* there), for invalid identifiers, or when `information_schema` is unreadable —
|
|
37
|
+
* the conservative answer "not known to exist" makes the caller fall through to
|
|
38
|
+
* its idempotent `CREATE TABLE IF NOT EXISTS`, preserving today's behaviour.
|
|
39
|
+
*
|
|
40
|
+
* This is a plain read (no lock), so it never blocks on an `ACCESS EXCLUSIVE`
|
|
41
|
+
* lock the way `CREATE`/`ALTER` would.
|
|
42
|
+
*/
|
|
43
|
+
export async function pgTableExists(table, injectedClient) {
|
|
44
|
+
if (!isPostgres() || !PLAIN_IDENTIFIER.test(table))
|
|
45
|
+
return false;
|
|
46
|
+
const client = injectedClient ?? getDbExec();
|
|
47
|
+
try {
|
|
48
|
+
const { rows } = await client.execute({
|
|
49
|
+
sql: `SELECT 1 FROM information_schema.tables
|
|
50
|
+
WHERE table_schema = 'public' AND table_name = ? LIMIT 1`,
|
|
51
|
+
args: [table],
|
|
52
|
+
});
|
|
53
|
+
return rows.length > 0;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// information_schema unreadable (permissions / non-standard backend) —
|
|
57
|
+
// report "unknown" so the caller falls back to IF NOT EXISTS.
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* True when running against Postgres AND the given column already exists on the
|
|
63
|
+
* given table in the `public` schema. Returns `false` on SQLite, for invalid
|
|
64
|
+
* identifiers, or when `information_schema` is unreadable.
|
|
65
|
+
*
|
|
66
|
+
* Plain read — no lock taken.
|
|
67
|
+
*/
|
|
68
|
+
export async function pgColumnExists(table, column, injectedClient) {
|
|
69
|
+
if (!isPostgres())
|
|
70
|
+
return false;
|
|
71
|
+
if (!PLAIN_IDENTIFIER.test(table) || !PLAIN_IDENTIFIER.test(column)) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
const client = injectedClient ?? getDbExec();
|
|
75
|
+
try {
|
|
76
|
+
const { rows } = await client.execute({
|
|
77
|
+
sql: `SELECT 1 FROM information_schema.columns
|
|
78
|
+
WHERE table_schema = 'public' AND table_name = ? AND column_name = ?
|
|
79
|
+
LIMIT 1`,
|
|
80
|
+
args: [table, column],
|
|
81
|
+
});
|
|
82
|
+
return rows.length > 0;
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* True when running against Postgres AND an index with the given name already
|
|
90
|
+
* exists in the `public` schema. Returns `false` on SQLite, for invalid
|
|
91
|
+
* identifiers, or when `pg_indexes` is unreadable.
|
|
92
|
+
*
|
|
93
|
+
* `CREATE INDEX` (without CONCURRENTLY) takes a `SHARE` lock that blocks
|
|
94
|
+
* writes, so on a fresh background-worker process behind a concurrent
|
|
95
|
+
* connection this can hang just like a `CREATE`/`ALTER` would; checking first
|
|
96
|
+
* skips the lock on the already-migrated hot path.
|
|
97
|
+
*/
|
|
98
|
+
export async function pgIndexExists(indexName, injectedClient) {
|
|
99
|
+
if (!isPostgres() || !PLAIN_IDENTIFIER.test(indexName))
|
|
100
|
+
return false;
|
|
101
|
+
const client = injectedClient ?? getDbExec();
|
|
102
|
+
try {
|
|
103
|
+
const { rows } = await client.execute({
|
|
104
|
+
sql: `SELECT 1 FROM pg_indexes
|
|
105
|
+
WHERE schemaname = 'public' AND indexname = ? LIMIT 1`,
|
|
106
|
+
args: [indexName],
|
|
107
|
+
});
|
|
108
|
+
return rows.length > 0;
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/** True when an error looks like a Postgres `lock_timeout` (SQLSTATE 55P03). */
|
|
115
|
+
export function isLockTimeoutError(err) {
|
|
116
|
+
const anyErr = err;
|
|
117
|
+
if (anyErr?.code === "55P03")
|
|
118
|
+
return true;
|
|
119
|
+
const msg = String(anyErr?.message ?? anyErr ?? "");
|
|
120
|
+
return /lock[_ ]?timeout|canceling statement due to lock timeout/i.test(msg);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Run a DDL statement that MUST execute (the schema is actually missing), with
|
|
124
|
+
* a short `lock_timeout` so a contended `ACCESS EXCLUSIVE` lock fails fast
|
|
125
|
+
* instead of hanging the whole process.
|
|
126
|
+
*
|
|
127
|
+
* Postgres path: wrap the DDL in an explicit transaction and set
|
|
128
|
+
* `SET LOCAL lock_timeout` so the timeout is scoped to THIS transaction only
|
|
129
|
+
* and reset automatically on COMMIT/ROLLBACK — it never leaks onto the pooled
|
|
130
|
+
* (session-reused) connection the way a bare `SET lock_timeout` would. If the
|
|
131
|
+
* DbExec has no `transaction` (shouldn't happen for Postgres, but defensively),
|
|
132
|
+
* fall back to a session `SET` + `RESET` in a finally. A lock-timeout error is
|
|
133
|
+
* swallowed: the table/column is virtually always already correct by the time a
|
|
134
|
+
* contended boot retries, and the caller's memoization should still resolve so
|
|
135
|
+
* the path isn't retried in a tight loop. Any non-lock-timeout error rethrows.
|
|
136
|
+
*
|
|
137
|
+
* SQLite path: no lock problem — just run the DDL directly.
|
|
138
|
+
*
|
|
139
|
+
* @returns `true` if the DDL ran to completion, `false` if it was skipped due to
|
|
140
|
+
* a lock-timeout (so the caller can decide whether to log).
|
|
141
|
+
*/
|
|
142
|
+
export async function runGuardedDdl(ddl, options = {}) {
|
|
143
|
+
const client = options.injectedClient ?? getDbExec();
|
|
144
|
+
if (!isPostgres()) {
|
|
145
|
+
await client.execute(ddl);
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
const lockTimeout = options.lockTimeout ?? "3s";
|
|
149
|
+
try {
|
|
150
|
+
if (typeof client.transaction === "function") {
|
|
151
|
+
await client.transaction(async (tx) => {
|
|
152
|
+
// SET LOCAL is transaction-scoped: it reverts on COMMIT/ROLLBACK and
|
|
153
|
+
// never persists on the pooled connection.
|
|
154
|
+
await tx.execute(`SET LOCAL lock_timeout = '${lockTimeout}'`);
|
|
155
|
+
await tx.execute(ddl);
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
// Defensive fallback: no transaction support. Use a session SET and
|
|
160
|
+
// guarantee a RESET so the timeout never leaks onto a reused connection.
|
|
161
|
+
try {
|
|
162
|
+
await client.execute(`SET lock_timeout = '${lockTimeout}'`);
|
|
163
|
+
await client.execute(ddl);
|
|
164
|
+
}
|
|
165
|
+
finally {
|
|
166
|
+
await client.execute(`RESET lock_timeout`).catch(() => { });
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
catch (err) {
|
|
172
|
+
if (isLockTimeoutError(err)) {
|
|
173
|
+
// Contended lock — the schema is virtually always already correct by now.
|
|
174
|
+
// Proceed; the caller's memoization still resolves so we don't loop.
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
throw err;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
//# sourceMappingURL=ddl-guard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ddl-guard.js","sourceRoot":"","sources":["../../src/db/ddl-guard.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Guards for on-demand `ensureTable()` DDL so the common already-migrated path\n * takes NO `ACCESS EXCLUSIVE` lock on Postgres.\n *\n * Lives in its own module (like `./widen-columns.js`) so stores can import it\n * without every `vi.mock(\"../db/client.js\")` test needing to stub it: the\n * helpers resolve `isPostgres()` / `getDbExec()` through `client.js`, so a test\n * that mocks the client to SQLite (`isPostgres: () => false`) makes the\n * Postgres-only existence checks no-ops automatically.\n *\n * ## Why\n *\n * `ensureTable()` runs once per process on first DB touch. In a long-lived Node\n * server the cost is paid once and is invisible. In a Netlify `-background`\n * function the process is fresh, so this is the FIRST touch — and the\n * `CREATE TABLE`/`ALTER TABLE ... ADD COLUMN` DDL it issues takes an\n * `ACCESS EXCLUSIVE` lock on the shared Neon Postgres database. Behind a\n * concurrent connection that already holds a conflicting lock, that DDL can\n * block ~indefinitely (observed >16s hangs in the bg worker vs ~1s inline).\n *\n * The tables/columns are essentially always already present in production, so\n * the DDL is redundant in the hot path. These helpers let a store cheaply check\n * `information_schema` first and only issue DDL when something is actually\n * missing — and when DDL must run, wrap it in a short `lock_timeout` so a\n * contended lock fails fast instead of hanging.\n *\n * All of this is Postgres-only behaviour gated on `isPostgres()`. On SQLite\n * (local dev) there is no such lock problem, so callers keep their existing\n * behaviour there.\n */\n\nimport { isPostgres, getDbExec, type DbExec } from \"./client.js\";\n\nconst PLAIN_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;\n\n/**\n * True when running against Postgres AND the given table already exists in the\n * `public` schema. Returns `false` on SQLite (callers gate their own behaviour\n * there), for invalid identifiers, or when `information_schema` is unreadable —\n * the conservative answer \"not known to exist\" makes the caller fall through to\n * its idempotent `CREATE TABLE IF NOT EXISTS`, preserving today's behaviour.\n *\n * This is a plain read (no lock), so it never blocks on an `ACCESS EXCLUSIVE`\n * lock the way `CREATE`/`ALTER` would.\n */\nexport async function pgTableExists(\n table: string,\n injectedClient?: DbExec,\n): Promise<boolean> {\n if (!isPostgres() || !PLAIN_IDENTIFIER.test(table)) return false;\n const client = injectedClient ?? getDbExec();\n try {\n const { rows } = await client.execute({\n sql: `SELECT 1 FROM information_schema.tables\n WHERE table_schema = 'public' AND table_name = ? LIMIT 1`,\n args: [table],\n });\n return rows.length > 0;\n } catch {\n // information_schema unreadable (permissions / non-standard backend) —\n // report \"unknown\" so the caller falls back to IF NOT EXISTS.\n return false;\n }\n}\n\n/**\n * True when running against Postgres AND the given column already exists on the\n * given table in the `public` schema. Returns `false` on SQLite, for invalid\n * identifiers, or when `information_schema` is unreadable.\n *\n * Plain read — no lock taken.\n */\nexport async function pgColumnExists(\n table: string,\n column: string,\n injectedClient?: DbExec,\n): Promise<boolean> {\n if (!isPostgres()) return false;\n if (!PLAIN_IDENTIFIER.test(table) || !PLAIN_IDENTIFIER.test(column)) {\n return false;\n }\n const client = injectedClient ?? getDbExec();\n try {\n const { rows } = await client.execute({\n sql: `SELECT 1 FROM information_schema.columns\n WHERE table_schema = 'public' AND table_name = ? AND column_name = ?\n LIMIT 1`,\n args: [table, column],\n });\n return rows.length > 0;\n } catch {\n return false;\n }\n}\n\n/**\n * True when running against Postgres AND an index with the given name already\n * exists in the `public` schema. Returns `false` on SQLite, for invalid\n * identifiers, or when `pg_indexes` is unreadable.\n *\n * `CREATE INDEX` (without CONCURRENTLY) takes a `SHARE` lock that blocks\n * writes, so on a fresh background-worker process behind a concurrent\n * connection this can hang just like a `CREATE`/`ALTER` would; checking first\n * skips the lock on the already-migrated hot path.\n */\nexport async function pgIndexExists(\n indexName: string,\n injectedClient?: DbExec,\n): Promise<boolean> {\n if (!isPostgres() || !PLAIN_IDENTIFIER.test(indexName)) return false;\n const client = injectedClient ?? getDbExec();\n try {\n const { rows } = await client.execute({\n sql: `SELECT 1 FROM pg_indexes\n WHERE schemaname = 'public' AND indexname = ? LIMIT 1`,\n args: [indexName],\n });\n return rows.length > 0;\n } catch {\n return false;\n }\n}\n\n/** True when an error looks like a Postgres `lock_timeout` (SQLSTATE 55P03). */\nexport function isLockTimeoutError(err: unknown): boolean {\n const anyErr = err as { code?: unknown; message?: unknown } | null;\n if (anyErr?.code === \"55P03\") return true;\n const msg = String(anyErr?.message ?? anyErr ?? \"\");\n return /lock[_ ]?timeout|canceling statement due to lock timeout/i.test(msg);\n}\n\n/**\n * Run a DDL statement that MUST execute (the schema is actually missing), with\n * a short `lock_timeout` so a contended `ACCESS EXCLUSIVE` lock fails fast\n * instead of hanging the whole process.\n *\n * Postgres path: wrap the DDL in an explicit transaction and set\n * `SET LOCAL lock_timeout` so the timeout is scoped to THIS transaction only\n * and reset automatically on COMMIT/ROLLBACK — it never leaks onto the pooled\n * (session-reused) connection the way a bare `SET lock_timeout` would. If the\n * DbExec has no `transaction` (shouldn't happen for Postgres, but defensively),\n * fall back to a session `SET` + `RESET` in a finally. A lock-timeout error is\n * swallowed: the table/column is virtually always already correct by the time a\n * contended boot retries, and the caller's memoization should still resolve so\n * the path isn't retried in a tight loop. Any non-lock-timeout error rethrows.\n *\n * SQLite path: no lock problem — just run the DDL directly.\n *\n * @returns `true` if the DDL ran to completion, `false` if it was skipped due to\n * a lock-timeout (so the caller can decide whether to log).\n */\nexport async function runGuardedDdl(\n ddl: string,\n options: { lockTimeout?: string; injectedClient?: DbExec } = {},\n): Promise<boolean> {\n const client = options.injectedClient ?? getDbExec();\n if (!isPostgres()) {\n await client.execute(ddl);\n return true;\n }\n\n const lockTimeout = options.lockTimeout ?? \"3s\";\n try {\n if (typeof client.transaction === \"function\") {\n await client.transaction(async (tx) => {\n // SET LOCAL is transaction-scoped: it reverts on COMMIT/ROLLBACK and\n // never persists on the pooled connection.\n await tx.execute(`SET LOCAL lock_timeout = '${lockTimeout}'`);\n await tx.execute(ddl);\n });\n } else {\n // Defensive fallback: no transaction support. Use a session SET and\n // guarantee a RESET so the timeout never leaks onto a reused connection.\n try {\n await client.execute(`SET lock_timeout = '${lockTimeout}'`);\n await client.execute(ddl);\n } finally {\n await client.execute(`RESET lock_timeout`).catch(() => {});\n }\n }\n return true;\n } catch (err) {\n if (isLockTimeoutError(err)) {\n // Contended lock — the schema is virtually always already correct by now.\n // Proceed; the caller's memoization still resolves so we don't loop.\n return false;\n }\n throw err;\n }\n}\n"]}
|
|
@@ -41,28 +41,28 @@ export declare function createObservabilityHandler(): import("h3").EventHandlerW
|
|
|
41
41
|
thumbsUpRate: number;
|
|
42
42
|
avgEvalScore: number;
|
|
43
43
|
} | {
|
|
44
|
-
error?: undefined;
|
|
45
|
-
ok?: undefined;
|
|
46
44
|
summary: import("./types.js").TraceSummary;
|
|
47
45
|
spans: import("./types.js").TraceSpan[];
|
|
48
46
|
id?: undefined;
|
|
49
|
-
} | {
|
|
50
47
|
error?: undefined;
|
|
51
48
|
ok?: undefined;
|
|
49
|
+
} | {
|
|
52
50
|
summary?: undefined;
|
|
53
51
|
spans?: undefined;
|
|
54
52
|
id: string;
|
|
55
|
-
|
|
53
|
+
error?: undefined;
|
|
56
54
|
ok?: undefined;
|
|
55
|
+
} | {
|
|
57
56
|
summary?: undefined;
|
|
58
57
|
spans?: undefined;
|
|
59
58
|
id?: undefined;
|
|
60
59
|
error: any;
|
|
60
|
+
ok?: undefined;
|
|
61
61
|
} | {
|
|
62
|
-
error?: undefined;
|
|
63
62
|
summary?: undefined;
|
|
64
63
|
spans?: undefined;
|
|
65
64
|
id?: undefined;
|
|
65
|
+
error?: undefined;
|
|
66
66
|
ok: boolean;
|
|
67
67
|
}>>;
|
|
68
68
|
//# sourceMappingURL=routes.d.ts.map
|
|
@@ -49,11 +49,11 @@ export declare function handleUpdateResource(event: any): Promise<import("./stor
|
|
|
49
49
|
}>;
|
|
50
50
|
/** DELETE /_agent-native/resources/:id — delete a resource */
|
|
51
51
|
export declare function handleDeleteResource(event: any): Promise<{
|
|
52
|
-
error: string;
|
|
53
52
|
ok?: undefined;
|
|
53
|
+
error: string;
|
|
54
54
|
} | {
|
|
55
|
-
ok: boolean;
|
|
56
55
|
error?: undefined;
|
|
56
|
+
ok: boolean;
|
|
57
57
|
}>;
|
|
58
58
|
/** POST /_agent-native/resources/upload — upload a file as a resource */
|
|
59
59
|
export declare function handleUploadResource(event: any): Promise<import("./store.js").Resource | {
|
|
@@ -73,9 +73,9 @@ export declare function handleUploadResource(event: any): Promise<import("./stor
|
|
|
73
73
|
runId: string | null;
|
|
74
74
|
expiresAt: number | null;
|
|
75
75
|
metadata: string | null;
|
|
76
|
+
error?: undefined;
|
|
76
77
|
url: string;
|
|
77
78
|
provider: string;
|
|
78
|
-
error?: undefined;
|
|
79
79
|
}>;
|
|
80
80
|
export {};
|
|
81
81
|
//# sourceMappingURL=handlers.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../../src/secrets/storage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;
|
|
1
|
+
{"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../../src/secrets/storage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAcH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AA8EjD;;;GAGG;AACH,wBAAgB,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAI3C;AAMD,MAAM,WAAW,SAAS;IACxB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,WAAW,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,eAAgB,SAAQ,SAAS;IAChD,KAAK,EAAE,MAAM,CAAC;IACd,kEAAkE;IAClE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,8DAA8D;IAC9D,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;;GAIG;AACH,wBAAsB,cAAc,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,CAAC,CA0C3E;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;GAGG;AACH,wBAAsB,aAAa,CACjC,GAAG,EAAE,SAAS,GACb,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAqBlC;AAED;;;;GAIG;AACH,wBAAsB,gBAAgB,CACpC,GAAG,EAAE,SAAS,GACb,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CAItD;AAED,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,WAAW,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,YAAY,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;GAIG;AACH,wBAAsB,iBAAiB,CACrC,GAAG,EAAE,SAAS,GACb,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CA2B5B;AAED;;;;GAIG;AACH,wBAAsB,sBAAsB,CAC1C,KAAK,EAAE,WAAW,EAClB,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,UAAU,EAAE,CAAC,CA0BvB;AAeD,wBAAsB,eAAe,CAAC,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC,CAStE"}
|
package/dist/secrets/storage.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { randomUUID } from "node:crypto";
|
|
15
15
|
import { getDbExec, isPostgres } from "../db/client.js";
|
|
16
|
+
import { pgColumnExists, pgTableExists, runGuardedDdl, } from "../db/ddl-guard.js";
|
|
16
17
|
import { encryptSecretValue as encryptValue, decryptSecretValue as decryptValue, } from "./crypto.js";
|
|
17
18
|
import { APP_SECRETS_CREATE_SQL } from "./schema.js";
|
|
18
19
|
// ---------------------------------------------------------------------------
|
|
@@ -25,10 +26,33 @@ async function ensureTable() {
|
|
|
25
26
|
const client = getDbExec();
|
|
26
27
|
// Postgres version of the CREATE TABLE — the generic `INTEGER` maps to
|
|
27
28
|
// BIGINT on Postgres, which we need for millisecond timestamps.
|
|
28
|
-
const
|
|
29
|
+
const createSql = isPostgres()
|
|
29
30
|
? APP_SECRETS_CREATE_SQL.replace(/\bINTEGER\b/g, "BIGINT")
|
|
30
31
|
: APP_SECRETS_CREATE_SQL;
|
|
31
|
-
|
|
32
|
+
if (isPostgres()) {
|
|
33
|
+
// Hot path: in production the table and both additive columns are
|
|
34
|
+
// virtually always already present. Issuing `CREATE`/`ALTER` would
|
|
35
|
+
// still take an ACCESS EXCLUSIVE lock — which, in a fresh background
|
|
36
|
+
// worker process behind a concurrent connection on the shared Neon DB,
|
|
37
|
+
// can block ~indefinitely. So check `information_schema` first (a plain
|
|
38
|
+
// read, no lock) and run DDL ONLY for what is actually missing. When
|
|
39
|
+
// DDL must run, `runGuardedDdl` wraps it in a transaction-scoped
|
|
40
|
+
// `lock_timeout` so a contended lock fails fast instead of hanging.
|
|
41
|
+
if (!(await pgTableExists("app_secrets"))) {
|
|
42
|
+
await runGuardedDdl(createSql);
|
|
43
|
+
}
|
|
44
|
+
if (!(await pgColumnExists("app_secrets", "description"))) {
|
|
45
|
+
await runGuardedDdl(`ALTER TABLE app_secrets ADD COLUMN IF NOT EXISTS description TEXT`);
|
|
46
|
+
}
|
|
47
|
+
if (!(await pgColumnExists("app_secrets", "url_allowlist"))) {
|
|
48
|
+
await runGuardedDdl(`ALTER TABLE app_secrets ADD COLUMN IF NOT EXISTS url_allowlist TEXT`);
|
|
49
|
+
}
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
// SQLite (local dev): no ACCESS EXCLUSIVE lock problem, keep the original
|
|
53
|
+
// create-then-additive-alter behaviour. SQLite has no
|
|
54
|
+
// `ADD COLUMN IF NOT EXISTS`, so the ALTERs stay wrapped in try/catch.
|
|
55
|
+
await client.execute(createSql);
|
|
32
56
|
// Additive migration: description column (for ad-hoc keys)
|
|
33
57
|
try {
|
|
34
58
|
await client.execute(`ALTER TABLE app_secrets ADD COLUMN description TEXT`);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"storage.js","sourceRoot":"","sources":["../../src/secrets/storage.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Storage layer for the framework secrets registry.\n *\n * Values are encrypted at rest with AES-256-GCM. The encryption key is\n * derived from `SECRETS_ENCRYPTION_KEY` (preferred) or the existing\n * `BETTER_AUTH_SECRET` env var (fallback so templates don't need a second\n * secret during development). If neither is set in production we fall back\n * to a machine-local key derived from the cwd — the secret is still only\n * readable on this machine, but consider setting `SECRETS_ENCRYPTION_KEY`\n * for a stable, rotatable key.\n *\n * Secret values are NEVER logged and NEVER returned from any route handler.\n */\n\nimport { randomUUID } from \"node:crypto\";\n\nimport { getDbExec, isPostgres } from \"../db/client.js\";\nimport {\n encryptSecretValue as encryptValue,\n decryptSecretValue as decryptValue,\n} from \"./crypto.js\";\nimport type { SecretScope } from \"./register.js\";\nimport { APP_SECRETS_CREATE_SQL } from \"./schema.js\";\n\n// ---------------------------------------------------------------------------\n// Table bootstrap\n// ---------------------------------------------------------------------------\n\nlet _initPromise: Promise<void> | undefined;\n\nasync function ensureTable(): Promise<void> {\n if (!_initPromise) {\n _initPromise = (async () => {\n const client = getDbExec();\n // Postgres version of the CREATE TABLE — the generic `INTEGER` maps to\n // BIGINT on Postgres, which we need for millisecond timestamps.\n const sql = isPostgres()\n ? APP_SECRETS_CREATE_SQL.replace(/\\bINTEGER\\b/g, \"BIGINT\")\n : APP_SECRETS_CREATE_SQL;\n await client.execute(sql);\n\n // Additive migration: description column (for ad-hoc keys)\n try {\n await client.execute(\n `ALTER TABLE app_secrets ADD COLUMN description TEXT`,\n );\n } catch {\n // Column already exists — expected\n }\n\n // Additive migration: url_allowlist column\n try {\n await client.execute(\n `ALTER TABLE app_secrets ADD COLUMN url_allowlist TEXT`,\n );\n } catch {\n // Column already exists — expected\n }\n })().catch((err) => {\n _initPromise = undefined;\n throw err;\n });\n }\n return _initPromise;\n}\n\n// ---------------------------------------------------------------------------\n// Encryption — see ./crypto.ts (shared with per-user credentials)\n// ---------------------------------------------------------------------------\n\n/**\n * Return the last 4 characters of a secret, with any leading characters\n * masked. Used to show a preview without leaking the value.\n */\nexport function last4(value: string): string {\n if (!value) return \"\";\n if (value.length <= 4) return \"••••\";\n return \"••••\" + value.slice(-4);\n}\n\n// ---------------------------------------------------------------------------\n// CRUD\n// ---------------------------------------------------------------------------\n\nexport interface SecretRef {\n key: string;\n scope: SecretScope;\n scopeId: string;\n}\n\nexport interface WriteSecretArgs extends SecretRef {\n value: string;\n /** Optional human-readable description (used for ad-hoc keys). */\n description?: string;\n /** Optional JSON-stringified array of allowed URL origins. */\n urlAllowlist?: string;\n}\n\n/**\n * Write (insert or update) a secret. The value is encrypted before being\n * stored — the caller's plaintext is never persisted. Returns the new\n * record's id.\n */\nexport async function writeAppSecret(args: WriteSecretArgs): Promise<string> {\n await ensureTable();\n const { key, value, scope, scopeId, description, urlAllowlist } = args;\n if (!key || !value || !scope || !scopeId) {\n throw new Error(\n \"writeAppSecret: key, value, scope, and scopeId are all required\",\n );\n }\n const client = getDbExec();\n const now = Date.now();\n const encrypted = encryptValue(value);\n\n // Upsert by (scope, scope_id, key). Keep the existing row's id on update so\n // references stay stable.\n const { rows } = await client.execute({\n sql: `SELECT id FROM app_secrets WHERE scope = ? AND scope_id = ? AND key = ?`,\n args: [scope, scopeId, key],\n });\n if (rows.length > 0) {\n const id = rows[0].id as string;\n await client.execute({\n sql: `UPDATE app_secrets SET encrypted_value = ?, description = ?, url_allowlist = ?, updated_at = ? WHERE id = ?`,\n args: [encrypted, description ?? null, urlAllowlist ?? null, now, id],\n });\n return id;\n }\n const id = randomUUID();\n await client.execute({\n sql: `INSERT INTO app_secrets (id, scope, scope_id, key, encrypted_value, description, url_allowlist, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n args: [\n id,\n scope,\n scopeId,\n key,\n encrypted,\n description ?? null,\n urlAllowlist ?? null,\n now,\n now,\n ],\n });\n return id;\n}\n\nexport interface ReadSecretResult {\n value: string;\n last4: string;\n updatedAt: number;\n}\n\n/**\n * Read a secret's plaintext value. Returns null when not found. The caller\n * is responsible for never logging the returned value.\n */\nexport async function readAppSecret(\n ref: SecretRef,\n): Promise<ReadSecretResult | null> {\n await ensureTable();\n const { key, scope, scopeId } = ref;\n const client = getDbExec();\n const { rows } = await client.execute({\n sql: `SELECT encrypted_value, updated_at FROM app_secrets WHERE scope = ? AND scope_id = ? AND key = ? LIMIT 1`,\n args: [scope, scopeId, key],\n });\n if (rows.length === 0) return null;\n try {\n const value = decryptValue(rows[0].encrypted_value as string);\n return {\n value,\n last4: last4(value),\n updatedAt: Number(rows[0].updated_at ?? 0),\n };\n } catch {\n // Decryption failure — key rotated, tampered row, etc. Don't throw up the\n // stack in a way that could leak the ciphertext; just report missing.\n return null;\n }\n}\n\n/**\n * Return just the metadata for a secret (no value). Used by the list route so\n * the UI can show the \"Set\" pill and last-4 without the decrypted value going\n * over the wire.\n */\nexport async function getAppSecretMeta(\n ref: SecretRef,\n): Promise<{ last4: string; updatedAt: number } | null> {\n const result = await readAppSecret(ref);\n if (!result) return null;\n return { last4: result.last4, updatedAt: result.updatedAt };\n}\n\nexport interface SecretMeta {\n key: string;\n scope: SecretScope;\n scopeId: string;\n last4: string;\n description: string | null;\n urlAllowlist: string[] | null;\n createdAt: number;\n updatedAt: number;\n}\n\n/**\n * Read a secret's metadata, including ad-hoc fields (description, allowlist),\n * without ever decrypting or returning the plaintext value. Used by the\n * ad-hoc list route and any UI that wants to render a key tile.\n */\nexport async function readAppSecretMeta(\n ref: SecretRef,\n): Promise<SecretMeta | null> {\n await ensureTable();\n const { key, scope, scopeId } = ref;\n const client = getDbExec();\n const { rows } = await client.execute({\n sql: `SELECT encrypted_value, description, url_allowlist, created_at, updated_at FROM app_secrets WHERE scope = ? AND scope_id = ? AND key = ? LIMIT 1`,\n args: [scope, scopeId, key],\n });\n if (rows.length === 0) return null;\n const row = rows[0];\n let last4Value = \"\";\n try {\n const value = decryptValue(row.encrypted_value as string);\n last4Value = last4(value);\n } catch {\n last4Value = \"\";\n }\n return {\n key,\n scope,\n scopeId,\n last4: last4Value,\n description: (row.description as string | null) ?? null,\n urlAllowlist: parseAllowlist(row.url_allowlist as string | null),\n createdAt: Number(row.created_at ?? 0),\n updatedAt: Number(row.updated_at ?? 0),\n };\n}\n\n/**\n * List all secrets for a given scope. Returns metadata only — values are\n * never decrypted or returned. Used by the ad-hoc list route to surface\n * user-created keys.\n */\nexport async function listAppSecretsForScope(\n scope: SecretScope,\n scopeId: string,\n): Promise<SecretMeta[]> {\n await ensureTable();\n const client = getDbExec();\n const { rows } = await client.execute({\n sql: `SELECT key, encrypted_value, description, url_allowlist, created_at, updated_at FROM app_secrets WHERE scope = ? AND scope_id = ? ORDER BY updated_at DESC`,\n args: [scope, scopeId],\n });\n return rows.map((row) => {\n let last4Value = \"\";\n try {\n const value = decryptValue(row.encrypted_value as string);\n last4Value = last4(value);\n } catch {\n last4Value = \"\";\n }\n return {\n key: row.key as string,\n scope,\n scopeId,\n last4: last4Value,\n description: (row.description as string | null) ?? null,\n urlAllowlist: parseAllowlist(row.url_allowlist as string | null),\n createdAt: Number(row.created_at ?? 0),\n updatedAt: Number(row.updated_at ?? 0),\n };\n });\n}\n\nfunction parseAllowlist(raw: string | null): string[] | null {\n if (!raw) return null;\n try {\n const parsed = JSON.parse(raw);\n if (Array.isArray(parsed) && parsed.every((v) => typeof v === \"string\")) {\n return parsed;\n }\n return null;\n } catch {\n return null;\n }\n}\n\nexport async function deleteAppSecret(ref: SecretRef): Promise<boolean> {\n await ensureTable();\n const { key, scope, scopeId } = ref;\n const client = getDbExec();\n const { rowsAffected } = await client.execute({\n sql: `DELETE FROM app_secrets WHERE scope = ? AND scope_id = ? AND key = ?`,\n args: [scope, scopeId, key],\n });\n return rowsAffected > 0;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"storage.js","sourceRoot":"","sources":["../../src/secrets/storage.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Storage layer for the framework secrets registry.\n *\n * Values are encrypted at rest with AES-256-GCM. The encryption key is\n * derived from `SECRETS_ENCRYPTION_KEY` (preferred) or the existing\n * `BETTER_AUTH_SECRET` env var (fallback so templates don't need a second\n * secret during development). If neither is set in production we fall back\n * to a machine-local key derived from the cwd — the secret is still only\n * readable on this machine, but consider setting `SECRETS_ENCRYPTION_KEY`\n * for a stable, rotatable key.\n *\n * Secret values are NEVER logged and NEVER returned from any route handler.\n */\n\nimport { randomUUID } from \"node:crypto\";\n\nimport { getDbExec, isPostgres } from \"../db/client.js\";\nimport {\n pgColumnExists,\n pgTableExists,\n runGuardedDdl,\n} from \"../db/ddl-guard.js\";\nimport {\n encryptSecretValue as encryptValue,\n decryptSecretValue as decryptValue,\n} from \"./crypto.js\";\nimport type { SecretScope } from \"./register.js\";\nimport { APP_SECRETS_CREATE_SQL } from \"./schema.js\";\n\n// ---------------------------------------------------------------------------\n// Table bootstrap\n// ---------------------------------------------------------------------------\n\nlet _initPromise: Promise<void> | undefined;\n\nasync function ensureTable(): Promise<void> {\n if (!_initPromise) {\n _initPromise = (async () => {\n const client = getDbExec();\n // Postgres version of the CREATE TABLE — the generic `INTEGER` maps to\n // BIGINT on Postgres, which we need for millisecond timestamps.\n const createSql = isPostgres()\n ? APP_SECRETS_CREATE_SQL.replace(/\\bINTEGER\\b/g, \"BIGINT\")\n : APP_SECRETS_CREATE_SQL;\n\n if (isPostgres()) {\n // Hot path: in production the table and both additive columns are\n // virtually always already present. Issuing `CREATE`/`ALTER` would\n // still take an ACCESS EXCLUSIVE lock — which, in a fresh background\n // worker process behind a concurrent connection on the shared Neon DB,\n // can block ~indefinitely. So check `information_schema` first (a plain\n // read, no lock) and run DDL ONLY for what is actually missing. When\n // DDL must run, `runGuardedDdl` wraps it in a transaction-scoped\n // `lock_timeout` so a contended lock fails fast instead of hanging.\n if (!(await pgTableExists(\"app_secrets\"))) {\n await runGuardedDdl(createSql);\n }\n if (!(await pgColumnExists(\"app_secrets\", \"description\"))) {\n await runGuardedDdl(\n `ALTER TABLE app_secrets ADD COLUMN IF NOT EXISTS description TEXT`,\n );\n }\n if (!(await pgColumnExists(\"app_secrets\", \"url_allowlist\"))) {\n await runGuardedDdl(\n `ALTER TABLE app_secrets ADD COLUMN IF NOT EXISTS url_allowlist TEXT`,\n );\n }\n return;\n }\n\n // SQLite (local dev): no ACCESS EXCLUSIVE lock problem, keep the original\n // create-then-additive-alter behaviour. SQLite has no\n // `ADD COLUMN IF NOT EXISTS`, so the ALTERs stay wrapped in try/catch.\n await client.execute(createSql);\n\n // Additive migration: description column (for ad-hoc keys)\n try {\n await client.execute(\n `ALTER TABLE app_secrets ADD COLUMN description TEXT`,\n );\n } catch {\n // Column already exists — expected\n }\n\n // Additive migration: url_allowlist column\n try {\n await client.execute(\n `ALTER TABLE app_secrets ADD COLUMN url_allowlist TEXT`,\n );\n } catch {\n // Column already exists — expected\n }\n })().catch((err) => {\n _initPromise = undefined;\n throw err;\n });\n }\n return _initPromise;\n}\n\n// ---------------------------------------------------------------------------\n// Encryption — see ./crypto.ts (shared with per-user credentials)\n// ---------------------------------------------------------------------------\n\n/**\n * Return the last 4 characters of a secret, with any leading characters\n * masked. Used to show a preview without leaking the value.\n */\nexport function last4(value: string): string {\n if (!value) return \"\";\n if (value.length <= 4) return \"••••\";\n return \"••••\" + value.slice(-4);\n}\n\n// ---------------------------------------------------------------------------\n// CRUD\n// ---------------------------------------------------------------------------\n\nexport interface SecretRef {\n key: string;\n scope: SecretScope;\n scopeId: string;\n}\n\nexport interface WriteSecretArgs extends SecretRef {\n value: string;\n /** Optional human-readable description (used for ad-hoc keys). */\n description?: string;\n /** Optional JSON-stringified array of allowed URL origins. */\n urlAllowlist?: string;\n}\n\n/**\n * Write (insert or update) a secret. The value is encrypted before being\n * stored — the caller's plaintext is never persisted. Returns the new\n * record's id.\n */\nexport async function writeAppSecret(args: WriteSecretArgs): Promise<string> {\n await ensureTable();\n const { key, value, scope, scopeId, description, urlAllowlist } = args;\n if (!key || !value || !scope || !scopeId) {\n throw new Error(\n \"writeAppSecret: key, value, scope, and scopeId are all required\",\n );\n }\n const client = getDbExec();\n const now = Date.now();\n const encrypted = encryptValue(value);\n\n // Upsert by (scope, scope_id, key). Keep the existing row's id on update so\n // references stay stable.\n const { rows } = await client.execute({\n sql: `SELECT id FROM app_secrets WHERE scope = ? AND scope_id = ? AND key = ?`,\n args: [scope, scopeId, key],\n });\n if (rows.length > 0) {\n const id = rows[0].id as string;\n await client.execute({\n sql: `UPDATE app_secrets SET encrypted_value = ?, description = ?, url_allowlist = ?, updated_at = ? WHERE id = ?`,\n args: [encrypted, description ?? null, urlAllowlist ?? null, now, id],\n });\n return id;\n }\n const id = randomUUID();\n await client.execute({\n sql: `INSERT INTO app_secrets (id, scope, scope_id, key, encrypted_value, description, url_allowlist, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n args: [\n id,\n scope,\n scopeId,\n key,\n encrypted,\n description ?? null,\n urlAllowlist ?? null,\n now,\n now,\n ],\n });\n return id;\n}\n\nexport interface ReadSecretResult {\n value: string;\n last4: string;\n updatedAt: number;\n}\n\n/**\n * Read a secret's plaintext value. Returns null when not found. The caller\n * is responsible for never logging the returned value.\n */\nexport async function readAppSecret(\n ref: SecretRef,\n): Promise<ReadSecretResult | null> {\n await ensureTable();\n const { key, scope, scopeId } = ref;\n const client = getDbExec();\n const { rows } = await client.execute({\n sql: `SELECT encrypted_value, updated_at FROM app_secrets WHERE scope = ? AND scope_id = ? AND key = ? LIMIT 1`,\n args: [scope, scopeId, key],\n });\n if (rows.length === 0) return null;\n try {\n const value = decryptValue(rows[0].encrypted_value as string);\n return {\n value,\n last4: last4(value),\n updatedAt: Number(rows[0].updated_at ?? 0),\n };\n } catch {\n // Decryption failure — key rotated, tampered row, etc. Don't throw up the\n // stack in a way that could leak the ciphertext; just report missing.\n return null;\n }\n}\n\n/**\n * Return just the metadata for a secret (no value). Used by the list route so\n * the UI can show the \"Set\" pill and last-4 without the decrypted value going\n * over the wire.\n */\nexport async function getAppSecretMeta(\n ref: SecretRef,\n): Promise<{ last4: string; updatedAt: number } | null> {\n const result = await readAppSecret(ref);\n if (!result) return null;\n return { last4: result.last4, updatedAt: result.updatedAt };\n}\n\nexport interface SecretMeta {\n key: string;\n scope: SecretScope;\n scopeId: string;\n last4: string;\n description: string | null;\n urlAllowlist: string[] | null;\n createdAt: number;\n updatedAt: number;\n}\n\n/**\n * Read a secret's metadata, including ad-hoc fields (description, allowlist),\n * without ever decrypting or returning the plaintext value. Used by the\n * ad-hoc list route and any UI that wants to render a key tile.\n */\nexport async function readAppSecretMeta(\n ref: SecretRef,\n): Promise<SecretMeta | null> {\n await ensureTable();\n const { key, scope, scopeId } = ref;\n const client = getDbExec();\n const { rows } = await client.execute({\n sql: `SELECT encrypted_value, description, url_allowlist, created_at, updated_at FROM app_secrets WHERE scope = ? AND scope_id = ? AND key = ? LIMIT 1`,\n args: [scope, scopeId, key],\n });\n if (rows.length === 0) return null;\n const row = rows[0];\n let last4Value = \"\";\n try {\n const value = decryptValue(row.encrypted_value as string);\n last4Value = last4(value);\n } catch {\n last4Value = \"\";\n }\n return {\n key,\n scope,\n scopeId,\n last4: last4Value,\n description: (row.description as string | null) ?? null,\n urlAllowlist: parseAllowlist(row.url_allowlist as string | null),\n createdAt: Number(row.created_at ?? 0),\n updatedAt: Number(row.updated_at ?? 0),\n };\n}\n\n/**\n * List all secrets for a given scope. Returns metadata only — values are\n * never decrypted or returned. Used by the ad-hoc list route to surface\n * user-created keys.\n */\nexport async function listAppSecretsForScope(\n scope: SecretScope,\n scopeId: string,\n): Promise<SecretMeta[]> {\n await ensureTable();\n const client = getDbExec();\n const { rows } = await client.execute({\n sql: `SELECT key, encrypted_value, description, url_allowlist, created_at, updated_at FROM app_secrets WHERE scope = ? AND scope_id = ? ORDER BY updated_at DESC`,\n args: [scope, scopeId],\n });\n return rows.map((row) => {\n let last4Value = \"\";\n try {\n const value = decryptValue(row.encrypted_value as string);\n last4Value = last4(value);\n } catch {\n last4Value = \"\";\n }\n return {\n key: row.key as string,\n scope,\n scopeId,\n last4: last4Value,\n description: (row.description as string | null) ?? null,\n urlAllowlist: parseAllowlist(row.url_allowlist as string | null),\n createdAt: Number(row.created_at ?? 0),\n updatedAt: Number(row.updated_at ?? 0),\n };\n });\n}\n\nfunction parseAllowlist(raw: string | null): string[] | null {\n if (!raw) return null;\n try {\n const parsed = JSON.parse(raw);\n if (Array.isArray(parsed) && parsed.every((v) => typeof v === \"string\")) {\n return parsed;\n }\n return null;\n } catch {\n return null;\n }\n}\n\nexport async function deleteAppSecret(ref: SecretRef): Promise<boolean> {\n await ensureTable();\n const { key, scope, scopeId } = ref;\n const client = getDbExec();\n const { rowsAffected } = await client.execute({\n sql: `DELETE FROM app_secrets WHERE scope = ? AND scope_id = ? AND key = ?`,\n args: [scope, scopeId, key],\n });\n return rowsAffected > 0;\n}\n"]}
|
|
@@ -23,15 +23,15 @@ export declare function resolveAgentEngineApiKeyWriteTarget(event: H3Event, scop
|
|
|
23
23
|
error: string;
|
|
24
24
|
}>;
|
|
25
25
|
export declare function createAgentEngineApiKeyHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
|
|
26
|
-
ok?: undefined;
|
|
27
26
|
error: any;
|
|
28
27
|
key?: undefined;
|
|
29
28
|
scope?: undefined;
|
|
29
|
+
ok?: undefined;
|
|
30
30
|
} | {
|
|
31
|
-
error?: undefined;
|
|
32
31
|
ok: boolean;
|
|
33
32
|
key: string;
|
|
34
33
|
scope: AgentEngineApiKeyScope;
|
|
34
|
+
error?: undefined;
|
|
35
35
|
}>>;
|
|
36
36
|
export {};
|
|
37
37
|
//# sourceMappingURL=agent-engine-api-key-route.d.ts.map
|
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
* Supported environment variables:
|
|
4
4
|
* - `GA_MEASUREMENT_ID` — Google Analytics 4 measurement ID
|
|
5
5
|
*
|
|
6
|
+
* Netlify configuration-file env vars are build-time only for serverless
|
|
7
|
+
* functions, so the Vite plugin also bakes this public value into SSR bundles
|
|
8
|
+
* as `__AGENT_NATIVE_BUILD_GA_MEASUREMENT_ID__`.
|
|
9
|
+
*
|
|
6
10
|
* Amplitude and Sentry are initialized client-side via their npm packages
|
|
7
11
|
* (see `packages/core/src/client/analytics.ts`). Only GA requires script
|
|
8
12
|
* tag injection because the gtag.js loader must be a `<script src>`.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"analytics.d.ts","sourceRoot":"","sources":["../../src/server/analytics.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"analytics.d.ts","sourceRoot":"","sources":["../../src/server/analytics.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAwCH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,cAAc,GAAG,cAAc,CA4BtE"}
|
package/dist/server/analytics.js
CHANGED
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
* Supported environment variables:
|
|
4
4
|
* - `GA_MEASUREMENT_ID` — Google Analytics 4 measurement ID
|
|
5
5
|
*
|
|
6
|
+
* Netlify configuration-file env vars are build-time only for serverless
|
|
7
|
+
* functions, so the Vite plugin also bakes this public value into SSR bundles
|
|
8
|
+
* as `__AGENT_NATIVE_BUILD_GA_MEASUREMENT_ID__`.
|
|
9
|
+
*
|
|
6
10
|
* Amplitude and Sentry are initialized client-side via their npm packages
|
|
7
11
|
* (see `packages/core/src/client/analytics.ts`). Only GA requires script
|
|
8
12
|
* tag injection because the gtag.js loader must be a `<script src>`.
|
|
@@ -16,16 +20,28 @@
|
|
|
16
20
|
* return new Response(wrapWithAnalytics(body), { ... });
|
|
17
21
|
* ```
|
|
18
22
|
*/
|
|
23
|
+
function normalizeMeasurementId(value) {
|
|
24
|
+
const trimmed = value?.trim();
|
|
25
|
+
return trimmed ? trimmed : null;
|
|
26
|
+
}
|
|
27
|
+
function getGaMeasurementId() {
|
|
28
|
+
return (normalizeMeasurementId(process.env.GA_MEASUREMENT_ID) ||
|
|
29
|
+
normalizeMeasurementId(typeof __AGENT_NATIVE_BUILD_GA_MEASUREMENT_ID__ === "string"
|
|
30
|
+
? __AGENT_NATIVE_BUILD_GA_MEASUREMENT_ID__
|
|
31
|
+
: undefined));
|
|
32
|
+
}
|
|
19
33
|
function getGaScript() {
|
|
20
|
-
const id =
|
|
34
|
+
const id = getGaMeasurementId();
|
|
21
35
|
if (!id)
|
|
22
36
|
return null;
|
|
23
|
-
|
|
37
|
+
const srcId = encodeURIComponent(id);
|
|
38
|
+
const jsId = JSON.stringify(id);
|
|
39
|
+
return (`<script async src="https://www.googletagmanager.com/gtag/js?id=${srcId}"></script>` +
|
|
24
40
|
`<script>` +
|
|
25
41
|
`window.dataLayer=window.dataLayer||[];` +
|
|
26
42
|
`function gtag(){dataLayer.push(arguments);}` +
|
|
27
43
|
`gtag('js',new Date());` +
|
|
28
|
-
`gtag('config'
|
|
44
|
+
`gtag('config',${jsId});` +
|
|
29
45
|
`if(typeof sessionStorage!=='undefined'&&sessionStorage.getItem('__an_signin')){` +
|
|
30
46
|
`sessionStorage.removeItem('__an_signin');` +
|
|
31
47
|
`gtag('event','sign_in');` +
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"analytics.js","sourceRoot":"","sources":["../../src/server/analytics.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"analytics.js","sourceRoot":"","sources":["../../src/server/analytics.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAIH,SAAS,sBAAsB,CAAC,KAAyB;IACvD,MAAM,OAAO,GAAG,KAAK,EAAE,IAAI,EAAE,CAAC;IAC9B,OAAO,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC;AAClC,CAAC;AAED,SAAS,kBAAkB;IACzB,OAAO,CACL,sBAAsB,CAAC,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAC;QACrD,sBAAsB,CACpB,OAAO,wCAAwC,KAAK,QAAQ;YAC1D,CAAC,CAAC,wCAAwC;YAC1C,CAAC,CAAC,SAAS,CACd,CACF,CAAC;AACJ,CAAC;AAED,SAAS,WAAW;IAClB,MAAM,EAAE,GAAG,kBAAkB,EAAE,CAAC;IAChC,IAAI,CAAC,EAAE;QAAE,OAAO,IAAI,CAAC;IACrB,MAAM,KAAK,GAAG,kBAAkB,CAAC,EAAE,CAAC,CAAC;IACrC,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;IAChC,OAAO,CACL,kEAAkE,KAAK,aAAa;QACpF,UAAU;QACV,wCAAwC;QACxC,6CAA6C;QAC7C,wBAAwB;QACxB,iBAAiB,IAAI,IAAI;QACzB,iFAAiF;QACjF,2CAA2C;QAC3C,0BAA0B;QAC1B,GAAG;QACH,WAAW,CACZ,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,IAAoB;IACpD,MAAM,OAAO,GAAG,CAAC,WAAW,EAAE,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACzD,IAAI,CAAC,OAAO;QAAE,OAAO,IAAI,CAAC;IAE1B,MAAM,OAAO,GAAG,IAAI,WAAW,EAAE,CAAC;IAClC,MAAM,OAAO,GAAG,IAAI,WAAW,EAAE,CAAC;IAClC,IAAI,QAAQ,GAAG,KAAK,CAAC;IAErB,OAAO,IAAI,CAAC,WAAW,CACrB,IAAI,eAAe,CAAC;QAClB,SAAS,CAAC,KAAK,EAAE,UAAU;YACzB,IAAI,QAAQ,EAAE,CAAC;gBACb,UAAU,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;gBAC1B,OAAO;YACT,CAAC;YACD,MAAM,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,KAAK,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;YACrD,MAAM,YAAY,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;YAC7C,IAAI,YAAY,KAAK,CAAC,CAAC,EAAE,CAAC;gBACxB,MAAM,QAAQ,GACZ,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,YAAY,CAAC,GAAG,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;gBACnE,UAAU,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC;gBAC7C,QAAQ,GAAG,IAAI,CAAC;YAClB,CAAC;iBAAM,CAAC;gBACN,UAAU,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;YAC5B,CAAC;QACH,CAAC;KACF,CAAC,CACH,CAAC;AACJ,CAAC","sourcesContent":["/**\n * Opt-in analytics injection for SSR streams.\n * Supported environment variables:\n * - `GA_MEASUREMENT_ID` — Google Analytics 4 measurement ID\n *\n * Netlify configuration-file env vars are build-time only for serverless\n * functions, so the Vite plugin also bakes this public value into SSR bundles\n * as `__AGENT_NATIVE_BUILD_GA_MEASUREMENT_ID__`.\n *\n * Amplitude and Sentry are initialized client-side via their npm packages\n * (see `packages/core/src/client/analytics.ts`). Only GA requires script\n * tag injection because the gtag.js loader must be a `<script src>`.\n *\n * When set, the corresponding script tags are injected before `</head>`.\n * When not set, the stream passes through untouched (zero overhead).\n *\n * Usage in entry.server.tsx:\n * ```ts\n * import { wrapWithAnalytics } from \"@agent-native/core/server\";\n * return new Response(wrapWithAnalytics(body), { ... });\n * ```\n */\n\ndeclare const __AGENT_NATIVE_BUILD_GA_MEASUREMENT_ID__: string | undefined;\n\nfunction normalizeMeasurementId(value: string | undefined): string | null {\n const trimmed = value?.trim();\n return trimmed ? trimmed : null;\n}\n\nfunction getGaMeasurementId(): string | null {\n return (\n normalizeMeasurementId(process.env.GA_MEASUREMENT_ID) ||\n normalizeMeasurementId(\n typeof __AGENT_NATIVE_BUILD_GA_MEASUREMENT_ID__ === \"string\"\n ? __AGENT_NATIVE_BUILD_GA_MEASUREMENT_ID__\n : undefined,\n )\n );\n}\n\nfunction getGaScript(): string | null {\n const id = getGaMeasurementId();\n if (!id) return null;\n const srcId = encodeURIComponent(id);\n const jsId = JSON.stringify(id);\n return (\n `<script async src=\"https://www.googletagmanager.com/gtag/js?id=${srcId}\"></script>` +\n `<script>` +\n `window.dataLayer=window.dataLayer||[];` +\n `function gtag(){dataLayer.push(arguments);}` +\n `gtag('js',new Date());` +\n `gtag('config',${jsId});` +\n `if(typeof sessionStorage!=='undefined'&&sessionStorage.getItem('__an_signin')){` +\n `sessionStorage.removeItem('__an_signin');` +\n `gtag('event','sign_in');` +\n `}` +\n `</script>`\n );\n}\n\nexport function wrapWithAnalytics(body: ReadableStream): ReadableStream {\n const scripts = [getGaScript()].filter(Boolean).join(\"\");\n if (!scripts) return body;\n\n const decoder = new TextDecoder();\n const encoder = new TextEncoder();\n let injected = false;\n\n return body.pipeThrough(\n new TransformStream({\n transform(chunk, controller) {\n if (injected) {\n controller.enqueue(chunk);\n return;\n }\n const text = decoder.decode(chunk, { stream: true });\n const headCloseIdx = text.indexOf(\"</head>\");\n if (headCloseIdx !== -1) {\n const modified =\n text.slice(0, headCloseIdx) + scripts + text.slice(headCloseIdx);\n controller.enqueue(encoder.encode(modified));\n injected = true;\n } else {\n controller.enqueue(chunk);\n }\n },\n }),\n );\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/settings/store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/settings/store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AActC,wBAAgB,kBAAkB,IAAI,YAAY,CAEjD;AAuED,wBAAsB,UAAU,CAC9B,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC,CAUzC;AAED,MAAM,WAAW,iBAAiB;IAChC,gEAAgE;IAChE,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,wBAAsB,UAAU,CAC9B,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,OAAO,CAAC,EAAE,iBAAiB,GAC1B,OAAO,CAAC,IAAI,CAAC,CAgBf;AAED,wBAAsB,aAAa,CACjC,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,iBAAiB,GAC1B,OAAO,CAAC,OAAO,CAAC,CAkBlB;AAED,wBAAsB,cAAc,IAAI,OAAO,CAC7C,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CACxC,CAUA"}
|
package/dist/settings/store.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { EventEmitter } from "events";
|
|
2
2
|
import { getDbExec, isPostgres, intType } from "../db/client.js";
|
|
3
|
+
import { pgIndexExists, pgTableExists, runGuardedDdl, } from "../db/ddl-guard.js";
|
|
3
4
|
import { widenIntColumnsToBigInt } from "../db/widen-columns.js";
|
|
4
5
|
let _initPromise;
|
|
5
6
|
const _emitter = new EventEmitter();
|
|
@@ -14,17 +15,42 @@ async function ensureTable() {
|
|
|
14
15
|
_initPromise = (async () => {
|
|
15
16
|
const client = getDbExec();
|
|
16
17
|
const table = settingsTable();
|
|
17
|
-
|
|
18
|
+
const createSql = `
|
|
18
19
|
CREATE TABLE IF NOT EXISTS ${table} (
|
|
19
20
|
key TEXT PRIMARY KEY,
|
|
20
21
|
value TEXT NOT NULL,
|
|
21
22
|
updated_at ${intType()} NOT NULL
|
|
22
23
|
)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
`;
|
|
25
|
+
if (isPostgres()) {
|
|
26
|
+
// Hot path: the `settings` table and its poll index are virtually
|
|
27
|
+
// always already present in production. Issuing `CREATE TABLE`/
|
|
28
|
+
// `CREATE INDEX` still takes a lock that, in a fresh background-worker
|
|
29
|
+
// process behind a concurrent connection on the shared Neon DB, can
|
|
30
|
+
// block ~indefinitely (ACCESS EXCLUSIVE for CREATE TABLE; a write-
|
|
31
|
+
// blocking SHARE lock for CREATE INDEX). So check `information_schema`/
|
|
32
|
+
// `pg_indexes` first (plain reads, no lock) and run DDL ONLY for what
|
|
33
|
+
// is actually missing. `runGuardedDdl` bounds any DDL that must run
|
|
34
|
+
// with a transaction-scoped `lock_timeout` so a contended lock fails
|
|
35
|
+
// fast instead of hanging. `settingsTable()` is `public.settings` on
|
|
36
|
+
// Postgres; the existence checks use the unqualified table name.
|
|
37
|
+
if (!(await pgTableExists("settings"))) {
|
|
38
|
+
await runGuardedDdl(createSql);
|
|
39
|
+
}
|
|
40
|
+
// Older deployments (pre BIGINT-compat) have a 32-bit `updated_at`; on
|
|
41
|
+
// Postgres the `Date.now()` written on every setSetting overflows int4.
|
|
42
|
+
// widenIntColumnsToBigInt already probes information_schema and only
|
|
43
|
+
// ALTERs columns that are still int4 — a no-op on fresh/widened DBs.
|
|
44
|
+
await widenIntColumnsToBigInt("settings", ["updated_at"]);
|
|
45
|
+
// Index for the poll watermark query: `SELECT MAX(updated_at)`.
|
|
46
|
+
if (!(await pgIndexExists("settings_updated_at_idx"))) {
|
|
47
|
+
await runGuardedDdl(`CREATE INDEX IF NOT EXISTS settings_updated_at_idx ON ${table} (updated_at)`);
|
|
48
|
+
}
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
// SQLite (local dev): no lock problem — keep the original behaviour.
|
|
52
|
+
await client.execute(createSql);
|
|
53
|
+
// No-op on SQLite (INTEGER is already 64-bit).
|
|
28
54
|
await widenIntColumnsToBigInt("settings", ["updated_at"]);
|
|
29
55
|
// Index for the poll watermark query: `SELECT MAX(updated_at) FROM settings`.
|
|
30
56
|
// MAX on an indexed column avoids a full-table scan on every poll cycle.
|