pipework 0.257.0 → 0.258.1
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/CHANGELOG.md +9 -0
- package/dist/cli/commands/alerts.js +2 -2
- package/dist/cli/commands/alerts.js.map +1 -1
- package/dist/cli/commands/db.js +1 -1
- package/dist/cli/commands/db.js.map +1 -1
- package/dist/core/validation/connectivity.d.ts.map +1 -1
- package/dist/core/validation/connectivity.js +1 -0
- package/dist/core/validation/connectivity.js.map +1 -1
- package/dist/core/validation/queue-tables.d.ts.map +1 -1
- package/dist/core/validation/queue-tables.js +4 -3
- package/dist/core/validation/queue-tables.js.map +1 -1
- package/dist/core/validation/rls-role.d.ts.map +1 -1
- package/dist/core/validation/rls-role.js +1 -0
- package/dist/core/validation/rls-role.js.map +1 -1
- package/dist/data/db/query-watch.js +1 -1
- package/dist/data/db/query-watch.js.map +1 -1
- package/dist/data/db/reservation.d.ts.map +1 -1
- package/dist/data/db/reservation.js +1 -0
- package/dist/data/db/reservation.js.map +1 -1
- package/dist/infra/email/inbound.d.ts +2 -0
- package/dist/infra/email/inbound.d.ts.map +1 -1
- package/dist/infra/email/inbound.js +1 -0
- package/dist/infra/email/inbound.js.map +1 -1
- package/dist/infra/support/handle.d.ts +2 -2
- package/dist/infra/support/handle.d.ts.map +1 -1
- package/dist/infra/support/handle.js +4 -2
- package/dist/infra/support/handle.js.map +1 -1
- package/dist/infra/support/namespace.d.ts.map +1 -1
- package/dist/test/reap.js +1 -1
- package/dist/test/reap.js.map +1 -1
- package/dist/test/reset.js +1 -1
- package/dist/test/reset.js.map +1 -1
- package/dist/test/setup.d.ts.map +1 -1
- package/dist/test/setup.js +6 -5
- package/dist/test/setup.js.map +1 -1
- package/dist/test/watch.js +3 -2
- package/dist/test/watch.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/commands/alerts.ts +2 -2
- package/src/cli/commands/db.ts +1 -1
- package/src/core/validation/connectivity.ts +1 -0
- package/src/core/validation/queue-tables.ts +4 -3
- package/src/core/validation/rls-role.ts +1 -0
- package/src/data/db/query-watch.ts +1 -1
- package/src/data/db/reservation.ts +1 -0
- package/src/infra/email/inbound.ts +3 -0
- package/src/infra/support/handle.ts +5 -4
- package/src/test/reap.ts +1 -1
- package/src/test/reset.ts +1 -1
- package/src/test/setup.ts +6 -5
- package/src/test/watch.ts +3 -2
|
@@ -200,6 +200,7 @@ export async function queueTableProblems(url: string, timeoutMs: number | null =
|
|
|
200
200
|
const { default: postgres } = await import('postgres')
|
|
201
201
|
const sql = postgres(url, {
|
|
202
202
|
max: 1,
|
|
203
|
+
fetch_types: false,
|
|
203
204
|
idle_timeout: 1,
|
|
204
205
|
...probeConnectTimeout(timeoutMs),
|
|
205
206
|
onnotice: () => {},
|
|
@@ -208,7 +209,7 @@ export async function queueTableProblems(url: string, timeoutMs: number | null =
|
|
|
208
209
|
try {
|
|
209
210
|
const presence = await sql<{ name: string; present: boolean }[]>`
|
|
210
211
|
SELECT t.name, to_regclass('public.' || quote_ident(t.name)) IS NOT NULL AS present
|
|
211
|
-
FROM
|
|
212
|
+
FROM jsonb_array_elements_text(${sql.json(tables)}) AS t(name)
|
|
212
213
|
`
|
|
213
214
|
const present = new Set(presence.filter(r => r.present).map(r => r.name))
|
|
214
215
|
|
|
@@ -240,12 +241,12 @@ export async function queueTableProblems(url: string, timeoutMs: number | null =
|
|
|
240
241
|
FROM pg_attribute a
|
|
241
242
|
JOIN pg_class c ON c.oid = a.attrelid
|
|
242
243
|
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
243
|
-
WHERE n.nspname = 'public' AND c.relname
|
|
244
|
+
WHERE n.nspname = 'public' AND c.relname IN (SELECT jsonb_array_elements_text(${sql.json([...present])}))
|
|
244
245
|
AND a.attnum > 0 AND NOT a.attisdropped
|
|
245
246
|
) AS columns,
|
|
246
247
|
(
|
|
247
248
|
SELECT coalesce(json_agg(json_build_object('name', t, 'oid', to_regtype(t)::oid::int)), '[]'::json)
|
|
248
|
-
FROM
|
|
249
|
+
FROM jsonb_array_elements_text(${sql.json(typeNames)}) AS t
|
|
249
250
|
) AS types
|
|
250
251
|
`
|
|
251
252
|
for (const column of row?.columns ?? []) {
|
|
@@ -39,6 +39,7 @@ export async function probeRlsRole(url: string, timeoutMs: number | null = 5000)
|
|
|
39
39
|
const { default: postgres } = await import('postgres')
|
|
40
40
|
const sql = postgres(url, {
|
|
41
41
|
max: 1,
|
|
42
|
+
fetch_types: false,
|
|
42
43
|
idle_timeout: 1,
|
|
43
44
|
...probeConnectTimeout(timeoutMs),
|
|
44
45
|
onnotice: () => {},
|
|
@@ -49,7 +49,7 @@ export function createStatementWatch(options: StatementWatchOptions): StatementW
|
|
|
49
49
|
function sampleBackend(sqlText: string, startedMs: number): Promise<BackendSample> {
|
|
50
50
|
if (sampling || closed) return Promise.resolve(UNKNOWN_BACKEND)
|
|
51
51
|
sampling = true
|
|
52
|
-
sampler ??= postgres(url, { max: 1, idle_timeout: 3, onnotice: () => {}, connect_timeout: 5 })
|
|
52
|
+
sampler ??= postgres(url, { max: 1, idle_timeout: 3, onnotice: () => {}, connect_timeout: 5, fetch_types: false })
|
|
53
53
|
const client = sampler
|
|
54
54
|
const run = client.unsafe<Array<{ pid: number; state: string | null; wait_event_type: string | null; wait_event: string | null }>>(
|
|
55
55
|
`SELECT pid, state, wait_event_type, wait_event FROM pg_stat_activity
|
|
@@ -96,6 +96,8 @@ export interface ReceivedEmail {
|
|
|
96
96
|
* and `from.address` is non-empty; a PASS on a message with an ambiguous From says nothing about any one author.
|
|
97
97
|
*/
|
|
98
98
|
readonly verdicts: EmailVerdicts | null
|
|
99
|
+
/** Which inbound source recorded the message; only the folder stand-in has no verdicts. */
|
|
100
|
+
readonly source: 'ses' | 'folder'
|
|
99
101
|
/** The envelope recipients SES accepted the message for; for the stand-in, the To and Cc addresses. */
|
|
100
102
|
readonly recipients: readonly string[]
|
|
101
103
|
}
|
|
@@ -493,6 +495,7 @@ export function createInbound(config: InboundConfig): Inbound {
|
|
|
493
495
|
contentId: a.contentId, inline: a.inline, file: null,
|
|
494
496
|
})),
|
|
495
497
|
verdicts, recipients,
|
|
498
|
+
source: row['source'] as 'ses' | 'folder',
|
|
496
499
|
}
|
|
497
500
|
|
|
498
501
|
let tenantId = (row['tenant_id'] as string | null) ?? null
|
|
@@ -65,7 +65,7 @@ export interface SupportConfig {
|
|
|
65
65
|
/**
|
|
66
66
|
* Decides what `receiveEmail` does with a message. Default: virus verdict `FAIL` drops it; DMARC `FAIL`, or SPF and
|
|
67
67
|
* DKIM both `FAIL`, separates it (the From address is probably forged, so it must not add text to a ticket an agent
|
|
68
|
-
* is reading: it always opens a new ticket); anything else, including no verdicts
|
|
68
|
+
* is reading: it always opens a new ticket); anything else, including no verdicts, is accepted (that is not trust: it only decides drop or separate). A message it
|
|
69
69
|
* separates never names a user: `resolveContact` is not called for it.
|
|
70
70
|
*/
|
|
71
71
|
readonly screen?: (email: ReceivedEmail) => ScreenVerdict
|
|
@@ -95,7 +95,7 @@ export interface SupportConfig {
|
|
|
95
95
|
/**
|
|
96
96
|
* With `joinThread: 'conversation'` (setting it with `'contact'` throws at create): whether a message from a sender
|
|
97
97
|
* other than the ticket's contact joins the ticket found. The contact's own message always joins. `'authentic'`
|
|
98
|
-
* (default): an email joins only when its DMARC verdict is `PASS
|
|
98
|
+
* (default): an email joins only when its DMARC verdict is `PASS`; null verdicts count as authentic only from the folder source (an SES mail without verdicts does not join);
|
|
99
99
|
* `receive` (chat/api) joins, since the app vouched for the caller. `'any'`: joins. A function decides itself, with
|
|
100
100
|
* `email: null` for `receive`. A refused message is handled like one `screen` separates: it opens its own ticket,
|
|
101
101
|
* names no user, and its `receive` audit has `separated: true` and `reason: 'not-authentic'`. It never touches the
|
|
@@ -535,8 +535,9 @@ export function createSupport(config: SupportConfig): Support {
|
|
|
535
535
|
const mayJoin = (t: Ticket): boolean => {
|
|
536
536
|
if (t.contactEmail === m.contactEmail || joinOthers === 'any') return true
|
|
537
537
|
if (typeof joinOthers === 'function') return joinOthers({ email: m.email ?? null, ticket: t })
|
|
538
|
-
|
|
539
|
-
|
|
538
|
+
if (m.email == null) return true
|
|
539
|
+
const v = m.email.verdicts
|
|
540
|
+
return v == null ? m.email.source === 'folder' : v.dmarc === 'PASS'
|
|
540
541
|
}
|
|
541
542
|
let ticket: Ticket | null = null
|
|
542
543
|
if (m.ticketId !== null) {
|
package/src/test/reap.ts
CHANGED
|
@@ -247,7 +247,7 @@ export async function reapConfiguredTestDatabases(instance: Manifold, options: R
|
|
|
247
247
|
const droppedDatabases: string[] = []
|
|
248
248
|
const skippedDatabases: string[] = []
|
|
249
249
|
for (const adminUrl of adminUrls) {
|
|
250
|
-
const client = postgres(adminUrl, { max: 1, onnotice: () => {} })
|
|
250
|
+
const client = postgres(adminUrl, { max: 1, fetch_types: false, onnotice: () => {} })
|
|
251
251
|
try {
|
|
252
252
|
const result = await reapOrphanTestDatabases(client, new Set(), options)
|
|
253
253
|
droppedDatabases.push(...result.droppedDatabases)
|
package/src/test/reset.ts
CHANGED
|
@@ -110,7 +110,7 @@ async function rlsAccess(q: Query, wanted: ReadonlySet<string>): Promise<{ lift:
|
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
function connect(url: string): postgres.Sql {
|
|
113
|
-
return postgres(url, { max: 1, onnotice: () => {}, idle_timeout: 0, max_lifetime: 0 })
|
|
113
|
+
return postgres(url, { max: 1, fetch_types: false, onnotice: () => {}, idle_timeout: 0, max_lifetime: 0 })
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
async function readCopy(sql: Query, table: string): Promise<Buffer> {
|
package/src/test/setup.ts
CHANGED
|
@@ -238,10 +238,10 @@ async function runSetup(instance: Manifold, tracker: PhaseTracker): Promise<Map<
|
|
|
238
238
|
async function prepareRun(adminUrl: string, liveTemplates: ReadonlySet<string>, tracker: PhaseTracker): Promise<void> {
|
|
239
239
|
runAdminUrl = adminUrl
|
|
240
240
|
if (adminClient === null) {
|
|
241
|
-
adminClient = postgres(adminUrl, { max: 1, onnotice: () => {} })
|
|
241
|
+
adminClient = postgres(adminUrl, { max: 1, fetch_types: false, onnotice: () => {} })
|
|
242
242
|
}
|
|
243
243
|
if (lockClient === null) {
|
|
244
|
-
lockClient = postgres(adminUrl, { max: 1, idle_timeout: 0, max_lifetime: 0, onnotice: () => {} })
|
|
244
|
+
lockClient = postgres(adminUrl, { max: 1, fetch_types: false, idle_timeout: 0, max_lifetime: 0, onnotice: () => {} })
|
|
245
245
|
await lockClient`SELECT pg_advisory_lock(${runLockKey(runNonce).toString()}::bigint)`
|
|
246
246
|
}
|
|
247
247
|
armWorkerShutdown()
|
|
@@ -416,6 +416,7 @@ async function templateIsReady(plan: TemplatePlan): Promise<boolean> {
|
|
|
416
416
|
async function readTemplateMarker(plan: TemplatePlan): Promise<boolean> {
|
|
417
417
|
const client = postgres(siblingUrl(plan.dbConfig, plan.templateDb), {
|
|
418
418
|
max: 1,
|
|
419
|
+
fetch_types: false,
|
|
419
420
|
onnotice: () => {},
|
|
420
421
|
})
|
|
421
422
|
try {
|
|
@@ -782,7 +783,7 @@ async function provisionTestSystemPolicy(
|
|
|
782
783
|
beat: (detail: string) => void,
|
|
783
784
|
): Promise<void> {
|
|
784
785
|
for (const url of migrateUrl === undefined ? [templateUrl] : [templateUrl, migrateUrl]) {
|
|
785
|
-
const client = postgres(url, { max: 1, onnotice: () => {} })
|
|
786
|
+
const client = postgres(url, { max: 1, fetch_types: false, onnotice: () => {} })
|
|
786
787
|
try {
|
|
787
788
|
const tables = await client<{ ident: string; name: string }[]>`
|
|
788
789
|
SELECT c.oid::regclass::text AS ident, c.relname AS name
|
|
@@ -1060,7 +1061,7 @@ async function provisionTemplateRole(
|
|
|
1060
1061
|
`END $$`,
|
|
1061
1062
|
)))
|
|
1062
1063
|
|
|
1063
|
-
const templateClient = postgres(templateUrl, { max: 1, onnotice: () => {} })
|
|
1064
|
+
const templateClient = postgres(templateUrl, { max: 1, fetch_types: false, onnotice: () => {} })
|
|
1064
1065
|
const templateDb = drizzle(templateClient)
|
|
1065
1066
|
|
|
1066
1067
|
await templateDb.execute(sql.raw(`GRANT USAGE ON SCHEMA public TO ${roleIdent}`))
|
|
@@ -1089,7 +1090,7 @@ async function provisionTemplateRole(
|
|
|
1089
1090
|
// grantor grants what it owns and nothing else.
|
|
1090
1091
|
await grantOwnedObjects(templateClient, roleIdent, APP_ROLE_GRANTS, beat)
|
|
1091
1092
|
|
|
1092
|
-
const migrateClient = postgres(migrateUrl, { max: 1, onnotice: () => {} })
|
|
1093
|
+
const migrateClient = postgres(migrateUrl, { max: 1, fetch_types: false, onnotice: () => {} })
|
|
1093
1094
|
try {
|
|
1094
1095
|
await grantOwnedObjects(migrateClient, roleIdent, APP_ROLE_GRANTS, beat)
|
|
1095
1096
|
// The test login is the role tests connect as, and it no longer owns what
|
package/src/test/watch.ts
CHANGED
|
@@ -74,7 +74,8 @@ async function sampleLine(target: TestWatchTarget): Promise<string> {
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
async function backends(adminUrl: string, databases: string[]): Promise<string[]> {
|
|
77
|
-
|
|
77
|
+
// A probe ended with a destroy timeout must not have an un-awaited type fetch in flight.
|
|
78
|
+
const client = postgres(adminUrl, { max: 1, onnotice: () => {}, connect_timeout: 5, fetch_types: false })
|
|
78
79
|
try {
|
|
79
80
|
const rows = await Promise.race([
|
|
80
81
|
client<BackendRow[]>`
|
|
@@ -82,7 +83,7 @@ async function backends(adminUrl: string, databases: string[]): Promise<string[]
|
|
|
82
83
|
round(extract(epoch FROM (now() - state_change))::numeric, 1)::text AS state_s,
|
|
83
84
|
pg_blocking_pids(pid)::text AS blocked_by, query
|
|
84
85
|
FROM pg_stat_activity
|
|
85
|
-
WHERE datname
|
|
86
|
+
WHERE datname IN (SELECT jsonb_array_elements_text(${client.json(databases)})) AND pid <> pg_backend_pid()
|
|
86
87
|
ORDER BY datname, pid`,
|
|
87
88
|
new Promise<never>((_, reject) => {
|
|
88
89
|
setTimeout(() => reject(new Error('pg_stat_activity query timed out')), WATCH_QUERY_TIMEOUT_MS).unref()
|