@cosmicdrift/kumiko-bundled-features 0.392.1 → 0.392.2
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/dist/inbound-mail-foundation/types.d.ts +1 -1
- package/dist/inbound-mail-foundation/watch-supervisor.js +16 -7
- package/dist/inbound-provider-inmemory/feature.d.ts +4 -1
- package/dist/inbound-provider-inmemory/feature.js +24 -2
- package/dist/inbound-provider-inmemory/index.d.ts +1 -1
- package/dist/inbound-provider-inmemory/index.js +1 -1
- package/package.json +11 -11
- package/src/inbound-mail-foundation/changes.json +12 -0
|
@@ -11,7 +11,7 @@ import type { OutboundMailMessage, OutboundMailSent } from "./outbound-mail.ts";
|
|
|
11
11
|
* secrets — tenant secret reads (IMAP password, OAuth refresh token)
|
|
12
12
|
* auditUserId — audit identity for secret reads. Handler path: unset,
|
|
13
13
|
* requireSecretsContext falls back to ctx.user.id; worker/provider
|
|
14
|
-
* path:
|
|
14
|
+
* path: the supervisor sets the system identity.
|
|
15
15
|
* metricsFor — metrics handle of a feature by name, same behavior as
|
|
16
16
|
* HandlerContext.metricsFor: unregistered metrics are a silent
|
|
17
17
|
* no-op instead of a throw.
|
|
@@ -44,7 +44,8 @@
|
|
|
44
44
|
// racing a new holder.
|
|
45
45
|
import { configuredPiiSubjectKms, decryptPiiFieldValues, } from "@cosmicdrift/kumiko-framework/crypto";
|
|
46
46
|
import { fetchOne, insertOne, selectMany, updateMany } from "@cosmicdrift/kumiko-framework/db";
|
|
47
|
-
import {
|
|
47
|
+
import { SYSTEM_USER_ID } from "@cosmicdrift/kumiko-framework/engine";
|
|
48
|
+
import { systemScopedRunner, tenantScopedSecrets, } from "@cosmicdrift/kumiko-framework/internal/row-level-security";
|
|
48
49
|
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
49
50
|
import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
|
|
50
51
|
import { InboundMailAccountStatuses, InboundMailFoundationHandlers } from "./constants.js";
|
|
@@ -80,13 +81,21 @@ export function createInboundMailSupervisor(deps) {
|
|
|
80
81
|
// configured TTL — real deployments (default 90s) never hit it.
|
|
81
82
|
const renewIntervalMs = Math.max(50, Math.floor((leaseTtlSeconds * 1000) / 3));
|
|
82
83
|
const log = deps.log ?? (() => { });
|
|
84
|
+
// The supervisor runs outside any request: secret reads need an audit
|
|
85
|
+
// identity, and under enforced row-level security they need the account's
|
|
86
|
+
// tenant scope, or the credential row is invisible and reads as missing.
|
|
87
|
+
const providerCtx = {
|
|
88
|
+
...deps.providerCtx,
|
|
89
|
+
auditUserId: deps.providerCtx.auditUserId ?? SYSTEM_USER_ID,
|
|
90
|
+
...(deps.providerCtx.secrets && { secrets: tenantScopedSecrets(deps.providerCtx.secrets) }),
|
|
91
|
+
};
|
|
83
92
|
const systemDb = systemScopedRunner(deps.db, "mail sync supervisor polls accounts and cursors of all tenants outside a request");
|
|
84
93
|
const oauthTokens = deps.oauthTokens ??
|
|
85
|
-
(
|
|
94
|
+
(providerCtx.secrets
|
|
86
95
|
? createOAuthAccessTokenManager({
|
|
87
96
|
db: systemDb,
|
|
88
|
-
secrets:
|
|
89
|
-
providerCtx
|
|
97
|
+
secrets: providerCtx.secrets,
|
|
98
|
+
providerCtx,
|
|
90
99
|
})
|
|
91
100
|
: undefined);
|
|
92
101
|
// Undefined for non-OAuth accounts. A refresh failure propagates into the
|
|
@@ -294,7 +303,7 @@ export function createInboundMailSupervisor(deps) {
|
|
|
294
303
|
// exhausted.
|
|
295
304
|
for (;;) {
|
|
296
305
|
const accessToken = await resolveAccessToken(account, plugin);
|
|
297
|
-
const result = await plugin.fetch(
|
|
306
|
+
const result = await plugin.fetch(providerCtx, account, cursor, {
|
|
298
307
|
backfillWindowDays,
|
|
299
308
|
maxMessages: budget,
|
|
300
309
|
filterUnknownMessageIds: unknownMessageIdsFilterFor(account),
|
|
@@ -317,7 +326,7 @@ export function createInboundMailSupervisor(deps) {
|
|
|
317
326
|
for (const account of accounts) {
|
|
318
327
|
let plugin;
|
|
319
328
|
try {
|
|
320
|
-
plugin = resolveInboundProviderForAccount(
|
|
329
|
+
plugin = resolveInboundProviderForAccount(providerCtx, account);
|
|
321
330
|
}
|
|
322
331
|
catch (err) {
|
|
323
332
|
log(`inbound-mail: ${describeError(err)}`);
|
|
@@ -461,7 +470,7 @@ export function createInboundMailSupervisor(deps) {
|
|
|
461
470
|
state.starting = true;
|
|
462
471
|
try {
|
|
463
472
|
const accessToken = await resolveAccessToken(account, plugin);
|
|
464
|
-
const stop = await plugin.watch(
|
|
473
|
+
const stop = await plugin.watch(providerCtx, account, {
|
|
465
474
|
onMessages: async (msgs) => {
|
|
466
475
|
try {
|
|
467
476
|
await ingestBatch(account, msgs, "watch");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type InboundMailProviderPlugin, type RawInboundMessage } from "@cosmicdrift/kumiko-bundled-features/inbound-mail-foundation";
|
|
2
2
|
export declare const INMEMORY_PROVIDER_KEY = "inmemory";
|
|
3
3
|
/** Test helper: adds a message to the mailbox. If a watch is active, it is
|
|
4
4
|
* pushed immediately (IDLE simulation); the next fetch also returns it by
|
|
@@ -9,6 +9,9 @@ export declare function seedInboundMessage(accountId: string, msg: RawInboundMes
|
|
|
9
9
|
export declare function failNextFetchWith(accountId: string, err: unknown): void;
|
|
10
10
|
/** Test helper: injects a one-shot error into the next verify. */
|
|
11
11
|
export declare function failNextVerifyWith(accountId: string, err: unknown): void;
|
|
12
|
+
/** Test helper: every fetch reads this tenant secret first, like a real
|
|
13
|
+
* provider reads its stored credentials; a missing secret is an auth error. */
|
|
14
|
+
export declare function requireCredentialSecretOnFetch(accountId: string, key: string): void;
|
|
12
15
|
/** Test helper: simulates the live connection terminating (the supervisor
|
|
13
16
|
* must restart with backoff). */
|
|
14
17
|
export declare function emitWatchError(accountId: string, err: unknown): void;
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
//
|
|
11
11
|
// **Pattern basis:** mail-transport-inmemory (module-level state +
|
|
12
12
|
// test-helper exports). Not for production.
|
|
13
|
+
import { InboundAuthError, } from "@cosmicdrift/kumiko-bundled-features/inbound-mail-foundation";
|
|
14
|
+
import { requireSecretsContext } from "@cosmicdrift/kumiko-bundled-features/secrets";
|
|
13
15
|
import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
14
16
|
const FEATURE_NAME = "inbound-provider-inmemory";
|
|
15
17
|
export const INMEMORY_PROVIDER_KEY = "inmemory";
|
|
@@ -17,7 +19,13 @@ const accounts = new Map();
|
|
|
17
19
|
function stateFor(accountId) {
|
|
18
20
|
let state = accounts.get(accountId);
|
|
19
21
|
if (!state) {
|
|
20
|
-
state = {
|
|
22
|
+
state = {
|
|
23
|
+
log: [],
|
|
24
|
+
watch: null,
|
|
25
|
+
nextFetchError: null,
|
|
26
|
+
nextVerifyError: null,
|
|
27
|
+
credentialSecretKey: null,
|
|
28
|
+
};
|
|
21
29
|
accounts.set(accountId, state);
|
|
22
30
|
}
|
|
23
31
|
return state;
|
|
@@ -40,6 +48,17 @@ export function failNextFetchWith(accountId, err) {
|
|
|
40
48
|
export function failNextVerifyWith(accountId, err) {
|
|
41
49
|
stateFor(accountId).nextVerifyError = err;
|
|
42
50
|
}
|
|
51
|
+
/** Test helper: every fetch reads this tenant secret first, like a real
|
|
52
|
+
* provider reads its stored credentials; a missing secret is an auth error. */
|
|
53
|
+
export function requireCredentialSecretOnFetch(accountId, key) {
|
|
54
|
+
stateFor(accountId).credentialSecretKey = key;
|
|
55
|
+
}
|
|
56
|
+
async function readCredentialSecret(ctx, account, key) {
|
|
57
|
+
const secrets = requireSecretsContext({ secrets: ctx.secrets, auditUserId: ctx.auditUserId }, FEATURE_NAME);
|
|
58
|
+
const secret = await secrets.get(account.tenantId, key);
|
|
59
|
+
if (!secret)
|
|
60
|
+
throw new InboundAuthError(`${FEATURE_NAME}: no credential secret ${key}`);
|
|
61
|
+
}
|
|
43
62
|
/** Test helper: simulates the live connection terminating (the supervisor
|
|
44
63
|
* must restart with backoff). */
|
|
45
64
|
export function emitWatchError(accountId, err) {
|
|
@@ -75,13 +94,16 @@ export const inMemoryInboundMailPlugin = {
|
|
|
75
94
|
throw err;
|
|
76
95
|
}
|
|
77
96
|
},
|
|
78
|
-
fetch: async (
|
|
97
|
+
fetch: async (ctx, account, cursor, opts) => {
|
|
79
98
|
const state = stateFor(account.id);
|
|
80
99
|
if (state.nextFetchError) {
|
|
81
100
|
const err = state.nextFetchError;
|
|
82
101
|
state.nextFetchError = null;
|
|
83
102
|
throw err;
|
|
84
103
|
}
|
|
104
|
+
if (state.credentialSecretKey) {
|
|
105
|
+
await readCredentialSecret(ctx, account, state.credentialSecretKey);
|
|
106
|
+
}
|
|
85
107
|
const offset = offsetOf(cursor);
|
|
86
108
|
const slice = state.log.slice(offset, offset + opts.maxMessages);
|
|
87
109
|
const nextOffset = offset + slice.length;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { createInboundProviderInMemoryFeature, emitWatchError, failNextFetchWith, failNextVerifyWith, INMEMORY_PROVIDER_KEY, isWatching, resetInboundInMemory, seedInboundMessage, } from "./feature.ts";
|
|
1
|
+
export { createInboundProviderInMemoryFeature, emitWatchError, failNextFetchWith, failNextVerifyWith, INMEMORY_PROVIDER_KEY, isWatching, requireCredentialSecretOnFetch, resetInboundInMemory, seedInboundMessage, } from "./feature.ts";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Public API of the inbound-provider-inmemory bundled-feature.
|
|
2
|
-
export { createInboundProviderInMemoryFeature, emitWatchError, failNextFetchWith, failNextVerifyWith, INMEMORY_PROVIDER_KEY, isWatching, resetInboundInMemory, seedInboundMessage, } from "./feature.js";
|
|
2
|
+
export { createInboundProviderInMemoryFeature, emitWatchError, failNextFetchWith, failNextVerifyWith, INMEMORY_PROVIDER_KEY, isWatching, requireCredentialSecretOnFetch, resetInboundInMemory, seedInboundMessage, } from "./feature.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-bundled-features",
|
|
3
|
-
"version": "0.392.
|
|
3
|
+
"version": "0.392.2",
|
|
4
4
|
"description": "Built-in features — tenant, user, auth, delivery. The stuff you'd rewrite anyway, already typed.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -558,9 +558,9 @@
|
|
|
558
558
|
}
|
|
559
559
|
},
|
|
560
560
|
"dependencies": {
|
|
561
|
-
"@cosmicdrift/kumiko-framework": "0.392.
|
|
562
|
-
"@cosmicdrift/kumiko-renderer": "0.392.
|
|
563
|
-
"@cosmicdrift/kumiko-types": "0.392.
|
|
561
|
+
"@cosmicdrift/kumiko-framework": "0.392.2",
|
|
562
|
+
"@cosmicdrift/kumiko-renderer": "0.392.2",
|
|
563
|
+
"@cosmicdrift/kumiko-types": "0.392.2",
|
|
564
564
|
"@mollie/api-client": "^4.5.0",
|
|
565
565
|
"@node-rs/argon2": "^2.0.2",
|
|
566
566
|
"@tiptap/core": "^3.31.3",
|
|
@@ -1142,17 +1142,17 @@
|
|
|
1142
1142
|
],
|
|
1143
1143
|
"devDependencies": {
|
|
1144
1144
|
"@testing-library/user-event": "^14.6.1",
|
|
1145
|
-
"@cosmicdrift/kumiko-locale-de": "0.392.
|
|
1146
|
-
"@cosmicdrift/kumiko-locale-es": "0.392.
|
|
1147
|
-
"@cosmicdrift/kumiko-locale-fr": "0.392.
|
|
1148
|
-
"@cosmicdrift/kumiko-locale-it": "0.392.
|
|
1149
|
-
"@cosmicdrift/kumiko-renderer-mail-html": "0.392.
|
|
1145
|
+
"@cosmicdrift/kumiko-locale-de": "0.392.2",
|
|
1146
|
+
"@cosmicdrift/kumiko-locale-es": "0.392.2",
|
|
1147
|
+
"@cosmicdrift/kumiko-locale-fr": "0.392.2",
|
|
1148
|
+
"@cosmicdrift/kumiko-locale-it": "0.392.2",
|
|
1149
|
+
"@cosmicdrift/kumiko-renderer-mail-html": "0.392.2",
|
|
1150
1150
|
"jsqr": "^1.4.0",
|
|
1151
1151
|
"unpdf": "^1.8.1",
|
|
1152
|
-
"@cosmicdrift/kumiko-testing": "0.392.
|
|
1152
|
+
"@cosmicdrift/kumiko-testing": "0.392.2"
|
|
1153
1153
|
},
|
|
1154
1154
|
"peerDependencies": {
|
|
1155
|
-
"@cosmicdrift/kumiko-testing": "0.392.
|
|
1155
|
+
"@cosmicdrift/kumiko-testing": "0.392.2"
|
|
1156
1156
|
},
|
|
1157
1157
|
"peerDependenciesMeta": {
|
|
1158
1158
|
"@cosmicdrift/kumiko-testing": {
|
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.392.2",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "Mail sync no longer fails when reading IMAP credentials",
|
|
6
|
+
"detail": "createInboundMailSupervisor builds its provider context with auditUserId defaulting to the system user. Apps that did not set auditUserId in providerCtx got an internal error on every fetch and watch because the secrets context needs an audit identity. An explicitly set auditUserId is kept."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"version": "0.392.2",
|
|
10
|
+
"type": "fix",
|
|
11
|
+
"title": "Mail sync finds the stored IMAP credentials under row-level security",
|
|
12
|
+
"detail": "createInboundMailSupervisor runs outside any request, so secret reads ran without a tenant scope and the application role saw no row in read_tenant_secrets. The supervisor now wraps providerCtx.secrets with tenantScopedSecrets, which the framework exports from internal/row-level-security. inbound-provider-inmemory gains requireCredentialSecretOnFetch so tests can exercise credential reads; a new integration test runs the secrets against an application-role database."
|
|
13
|
+
},
|
|
2
14
|
{
|
|
3
15
|
"version": "0.392.0",
|
|
4
16
|
"type": "fix",
|