@c9up/goldstone 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +238 -2
- package/dist/GoldstoneProvider.d.ts +33 -0
- package/dist/GoldstoneProvider.d.ts.map +1 -0
- package/dist/GoldstoneProvider.js +55 -0
- package/dist/GoldstoneProvider.js.map +1 -0
- package/dist/InboxManager.d.ts +17 -0
- package/dist/InboxManager.d.ts.map +1 -0
- package/dist/InboxManager.js +39 -0
- package/dist/InboxManager.js.map +1 -0
- package/dist/Mailbox.d.ts +83 -0
- package/dist/Mailbox.d.ts.map +1 -0
- package/dist/Mailbox.js +326 -0
- package/dist/Mailbox.js.map +1 -0
- package/dist/ReceivedMessage.d.ts +152 -0
- package/dist/ReceivedMessage.d.ts.map +1 -0
- package/dist/ReceivedMessage.js +179 -0
- package/dist/ReceivedMessage.js.map +1 -0
- package/dist/augmentations.d.ts +18 -0
- package/dist/augmentations.d.ts.map +1 -0
- package/dist/augmentations.js +10 -0
- package/dist/augmentations.js.map +1 -0
- package/dist/authenticity.d.ts +81 -0
- package/dist/authenticity.d.ts.map +1 -0
- package/dist/authenticity.js +102 -0
- package/dist/authenticity.js.map +1 -0
- package/dist/config.d.ts +85 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +50 -0
- package/dist/config.js.map +1 -0
- package/dist/errors.d.ts +15 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +50 -0
- package/dist/errors.js.map +1 -0
- package/dist/inbound/context.d.ts +43 -0
- package/dist/inbound/context.d.ts.map +1 -0
- package/dist/inbound/context.js +56 -0
- package/dist/inbound/context.js.map +1 -0
- package/dist/inbound/mailgun.d.ts +57 -0
- package/dist/inbound/mailgun.d.ts.map +1 -0
- package/dist/inbound/mailgun.js +132 -0
- package/dist/inbound/mailgun.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/native/generated.d.ts +106 -0
- package/dist/native/generated.d.ts.map +1 -0
- package/dist/native/generated.js +7 -0
- package/dist/native/generated.js.map +1 -0
- package/dist/native.d.ts +23 -0
- package/dist/native.d.ts.map +1 -0
- package/dist/native.js +45 -0
- package/dist/native.js.map +1 -0
- package/dist/services/main.d.ts +22 -0
- package/dist/services/main.d.ts.map +1 -0
- package/dist/services/main.js +43 -0
- package/dist/services/main.js.map +1 -0
- package/dist/vendor/nativeBinary.d.ts +67 -0
- package/dist/vendor/nativeBinary.d.ts.map +1 -0
- package/dist/vendor/nativeBinary.js +156 -0
- package/dist/vendor/nativeBinary.js.map +1 -0
- package/index.darwin-arm64.node +0 -0
- package/index.darwin-x64.node +0 -0
- package/index.linux-arm64-gnu.node +0 -0
- package/index.linux-x64-gnu.node +0 -0
- package/index.win32-x64-msvc.node +0 -0
- package/package.json +67 -3
- package/src/GoldstoneProvider.ts +75 -0
- package/src/InboxManager.ts +51 -0
- package/src/Mailbox.ts +433 -0
- package/src/ReceivedMessage.ts +287 -0
- package/src/augmentations.ts +22 -0
- package/src/authenticity.ts +183 -0
- package/src/config.ts +132 -0
- package/src/errors.ts +57 -0
- package/src/inbound/context.ts +92 -0
- package/src/inbound/mailgun.ts +201 -0
- package/src/index.ts +53 -0
- package/src/native/generated.ts +141 -0
- package/src/native.ts +62 -0
- package/src/services/main.ts +51 -0
- package/src/vendor/nativeBinary.ts +186 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What an inbound handler needs from the HTTP request, structurally — a
|
|
3
|
+
* Ream `HttpContext` fits, and so does a plain object in a test.
|
|
4
|
+
*/
|
|
5
|
+
export interface InboundResponse {
|
|
6
|
+
status(code: number): InboundResponse;
|
|
7
|
+
json(data: unknown): void;
|
|
8
|
+
}
|
|
9
|
+
export interface InboundHttpContext {
|
|
10
|
+
request: {
|
|
11
|
+
/**
|
|
12
|
+
* The parsed BODY only — never merged with the query string, where a
|
|
13
|
+
* caller could set a field the provider did not sign.
|
|
14
|
+
*/
|
|
15
|
+
body(): unknown;
|
|
16
|
+
};
|
|
17
|
+
response: InboundResponse;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Remembers the tokens of deliveries already handled, so a captured
|
|
21
|
+
* request cannot be replayed. The in-memory default covers one process;
|
|
22
|
+
* several instances need a shared store (Redis…) behind this interface.
|
|
23
|
+
*/
|
|
24
|
+
export interface ReplayGuard {
|
|
25
|
+
/**
|
|
26
|
+
* Take the token for `ttlSeconds`. `false` when it is already taken — a
|
|
27
|
+
* delivery handled, or being handled right now. Must be atomic: two
|
|
28
|
+
* requests racing with the same token must not both get `true`.
|
|
29
|
+
*/
|
|
30
|
+
claim(token: string, ttlSeconds: number): boolean | Promise<boolean>;
|
|
31
|
+
/** Give a token back, so a delivery whose handling failed can be retried. */
|
|
32
|
+
release(token: string): void | Promise<void>;
|
|
33
|
+
}
|
|
34
|
+
/** One process's tokens, each kept for its TTL, at most `capacity` of them. */
|
|
35
|
+
export declare class MemoryReplayGuard implements ReplayGuard {
|
|
36
|
+
#private;
|
|
37
|
+
constructor(capacity?: number);
|
|
38
|
+
claim(token: string, ttlSeconds: number): boolean;
|
|
39
|
+
release(token: string): void;
|
|
40
|
+
}
|
|
41
|
+
/** A body field as a single string, or `undefined`. */
|
|
42
|
+
export declare function field(body: unknown, name: string): string | undefined;
|
|
43
|
+
//# sourceMappingURL=context.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../src/inbound/context.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,MAAM,WAAW,eAAe;IAC/B,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,eAAe,CAAC;IACtC,IAAI,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,CAAC;CAC1B;AAED,MAAM,WAAW,kBAAkB;IAClC,OAAO,EAAE;QACR;;;WAGG;QACH,IAAI,IAAI,OAAO,CAAC;KAChB,CAAC;IACF,QAAQ,EAAE,eAAe,CAAC;CAC1B;AAED;;;;GAIG;AACH,MAAM,WAAW,WAAW;IAC3B;;;;OAIG;IACH,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACrE,6EAA6E;IAC7E,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC7C;AAED,+EAA+E;AAC/E,qBAAa,iBAAkB,YAAW,WAAW;;gBAIxC,QAAQ,SAAU;IAI9B,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO;IAejD,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;CAmB5B;AAED,uDAAuD;AACvD,wBAAgB,KAAK,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAMrE"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What an inbound handler needs from the HTTP request, structurally — a
|
|
3
|
+
* Ream `HttpContext` fits, and so does a plain object in a test.
|
|
4
|
+
*/
|
|
5
|
+
import { GoldstoneError } from "../errors.js";
|
|
6
|
+
/** One process's tokens, each kept for its TTL, at most `capacity` of them. */
|
|
7
|
+
export class MemoryReplayGuard {
|
|
8
|
+
#capacity;
|
|
9
|
+
#tokens = new Map();
|
|
10
|
+
constructor(capacity = 100_000) {
|
|
11
|
+
this.#capacity = capacity;
|
|
12
|
+
}
|
|
13
|
+
claim(token, ttlSeconds) {
|
|
14
|
+
// NaN would store a time no comparison is ever true of: the token
|
|
15
|
+
// would be claimable again at once.
|
|
16
|
+
if (!(Number.isFinite(ttlSeconds) && ttlSeconds > 0)) {
|
|
17
|
+
throw new GoldstoneError("E_GOLDSTONE_INVALID_OPTIONS", `MemoryReplayGuard: the TTL must be a positive number of seconds, got ${String(ttlSeconds)}`);
|
|
18
|
+
}
|
|
19
|
+
const until = this.#tokens.get(token);
|
|
20
|
+
if (until !== undefined && until > Date.now())
|
|
21
|
+
return false;
|
|
22
|
+
this.#store(token, ttlSeconds);
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
release(token) {
|
|
26
|
+
this.#tokens.delete(token);
|
|
27
|
+
}
|
|
28
|
+
#store(token, ttlSeconds) {
|
|
29
|
+
if (this.#tokens.size >= this.#capacity) {
|
|
30
|
+
const now = Date.now();
|
|
31
|
+
for (const [key, until] of this.#tokens) {
|
|
32
|
+
if (until <= now)
|
|
33
|
+
this.#tokens.delete(key);
|
|
34
|
+
}
|
|
35
|
+
// Still full: drop the oldest. Map keeps insertion order.
|
|
36
|
+
while (this.#tokens.size >= this.#capacity) {
|
|
37
|
+
const oldest = this.#tokens.keys().next();
|
|
38
|
+
if (oldest.done)
|
|
39
|
+
break;
|
|
40
|
+
this.#tokens.delete(oldest.value);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
this.#tokens.set(token, Date.now() + ttlSeconds * 1000);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/** A body field as a single string, or `undefined`. */
|
|
47
|
+
export function field(body, name) {
|
|
48
|
+
if (typeof body !== "object" || body === null)
|
|
49
|
+
return undefined;
|
|
50
|
+
const value = Reflect.get(body, name);
|
|
51
|
+
if (typeof value === "string")
|
|
52
|
+
return value;
|
|
53
|
+
// A field sent twice arrives as a list: ambiguous, refused.
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=context.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.js","sourceRoot":"","sources":["../../src/inbound/context.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAkC9C,+EAA+E;AAC/E,MAAM,OAAO,iBAAiB;IACpB,SAAS,CAAS;IAClB,OAAO,GAAG,IAAI,GAAG,EAAkB,CAAC;IAE7C,YAAY,QAAQ,GAAG,OAAO;QAC7B,IAAI,CAAC,SAAS,GAAG,QAAQ,CAAC;IAC3B,CAAC;IAED,KAAK,CAAC,KAAa,EAAE,UAAkB;QACtC,kEAAkE;QAClE,oCAAoC;QACpC,IAAI,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI,UAAU,GAAG,CAAC,CAAC,EAAE,CAAC;YACtD,MAAM,IAAI,cAAc,CACvB,6BAA6B,EAC7B,wEAAwE,MAAM,CAAC,UAAU,CAAC,EAAE,CAC5F,CAAC;QACH,CAAC;QACD,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QACtC,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE;YAAE,OAAO,KAAK,CAAC;QAC5D,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC;QAC/B,OAAO,IAAI,CAAC;IACb,CAAC;IAED,OAAO,CAAC,KAAa;QACpB,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAC5B,CAAC;IAED,MAAM,CAAC,KAAa,EAAE,UAAkB;QACvC,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;YACzC,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;YACvB,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;gBACzC,IAAI,KAAK,IAAI,GAAG;oBAAE,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YAC5C,CAAC;YACD,0DAA0D;YAC1D,OAAO,IAAI,CAAC,OAAO,CAAC,IAAI,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;gBAC5C,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC;gBAC1C,IAAI,MAAM,CAAC,IAAI;oBAAE,MAAM;gBACvB,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YACnC,CAAC;QACF,CAAC;QACD,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,UAAU,GAAG,IAAI,CAAC,CAAC;IACzD,CAAC;CACD;AAED,uDAAuD;AACvD,MAAM,UAAU,KAAK,CAAC,IAAa,EAAE,IAAY;IAChD,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,IAAI;QAAE,OAAO,SAAS,CAAC;IAChE,MAAM,KAAK,GAAY,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IAC/C,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAC5C,4DAA4D;IAC5D,OAAO,SAAS,CAAC;AAClB,CAAC"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mailgun inbound: a route that forwards to the application receives every
|
|
3
|
+
* mail as a form POST — the raw message in `body-mime`.
|
|
4
|
+
*
|
|
5
|
+
* router.post('/inbound/mailgun/mime', createMailgunInboundHandler({
|
|
6
|
+
* signingKey: env.get('MAILGUN_SIGNING_KEY'),
|
|
7
|
+
* async onMessage(message, { recipient }) { … },
|
|
8
|
+
* }))
|
|
9
|
+
*
|
|
10
|
+
* Configure the Mailgun route to forward to a URL ending in `mime`: that is
|
|
11
|
+
* what makes Mailgun send `body-mime`, the message as received.
|
|
12
|
+
*
|
|
13
|
+
* The fields are read from `ctx.request.body()`, which holds them only once
|
|
14
|
+
* Ream's body parser has read the form Mailgun posts (multipart/form-data or
|
|
15
|
+
* urlencoded): the route must run behind `BodyParserMiddleware`, with form
|
|
16
|
+
* and multipart limits above the largest message accepted. Without it every
|
|
17
|
+
* delivery is answered 401, its fields missing.
|
|
18
|
+
*
|
|
19
|
+
* Mailgun signs `timestamp` + `token` with the webhook signing key — and
|
|
20
|
+
* nothing else: the signature does not cover the message. So a signature is
|
|
21
|
+
* only worth what its freshness and uniqueness are worth, and both are
|
|
22
|
+
* enforced: a timestamp older than `maxAgeSeconds` is refused, and a token is
|
|
23
|
+
* handled once — claimed before handling, so two requests racing with it
|
|
24
|
+
* cannot both get through. A delivery whose handling failed gives its token
|
|
25
|
+
* back, so Mailgun's retry is handled; one already handled is acknowledged
|
|
26
|
+
* and not handled again.
|
|
27
|
+
*/
|
|
28
|
+
import type { AuthenticateOptions } from "../authenticity.js";
|
|
29
|
+
import { type Limits, type ParseOptions, type ReceivedMessage } from "../ReceivedMessage.js";
|
|
30
|
+
import { type InboundHttpContext, type ReplayGuard } from "./context.js";
|
|
31
|
+
export interface InboundMeta {
|
|
32
|
+
/** The SMTP envelope recipient Mailgun received it for. */
|
|
33
|
+
recipient?: string;
|
|
34
|
+
/** The SMTP envelope sender (return path). */
|
|
35
|
+
sender?: string;
|
|
36
|
+
}
|
|
37
|
+
export interface MailgunInboundOptions {
|
|
38
|
+
/** The webhook signing key (Mailgun → Settings → Webhooks). */
|
|
39
|
+
signingKey: string;
|
|
40
|
+
onMessage: (message: ReceivedMessage, meta: InboundMeta) => unknown;
|
|
41
|
+
/**
|
|
42
|
+
* A signed request older than this is refused. An integer from 1 to
|
|
43
|
+
* 86 400; default 120 (Mailgun's own guidance).
|
|
44
|
+
*/
|
|
45
|
+
maxAgeSeconds?: number;
|
|
46
|
+
/** Default: tokens kept in this process's memory. */
|
|
47
|
+
replayGuard?: ReplayGuard;
|
|
48
|
+
limits?: Limits;
|
|
49
|
+
/** DKIM / DMARC on each message (`message.authenticity`). Default `true`. */
|
|
50
|
+
authenticate?: boolean | Omit<AuthenticateOptions, "ip" | "helo">;
|
|
51
|
+
/** Remote content in `message.html`; see {@link ParseOptions}. Default `"block"`. */
|
|
52
|
+
remoteContent?: ParseOptions["remoteContent"];
|
|
53
|
+
}
|
|
54
|
+
type Handler = (ctx: InboundHttpContext, next?: () => Promise<void>) => Promise<void>;
|
|
55
|
+
export declare function createMailgunInboundHandler(options: MailgunInboundOptions): Handler;
|
|
56
|
+
export {};
|
|
57
|
+
//# sourceMappingURL=mailgun.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mailgun.d.ts","sourceRoot":"","sources":["../../src/inbound/mailgun.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAIH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAE9D,OAAO,EACN,KAAK,MAAM,EACX,KAAK,YAAY,EACjB,KAAK,eAAe,EAEpB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAEN,KAAK,kBAAkB,EAEvB,KAAK,WAAW,EAChB,MAAM,cAAc,CAAC;AAEtB,MAAM,WAAW,WAAW;IAC3B,2DAA2D;IAC3D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,8CAA8C;IAC9C,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,qBAAqB;IACrC,+DAA+D;IAC/D,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,CAAC,OAAO,EAAE,eAAe,EAAE,IAAI,EAAE,WAAW,KAAK,OAAO,CAAC;IACpE;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,qDAAqD;IACrD,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,6EAA6E;IAC7E,YAAY,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC,mBAAmB,EAAE,IAAI,GAAG,MAAM,CAAC,CAAC;IAClE,qFAAqF;IACrF,aAAa,CAAC,EAAE,YAAY,CAAC,eAAe,CAAC,CAAC;CAC9C;AAED,KAAK,OAAO,GAAG,CACd,GAAG,EAAE,kBAAkB,EACvB,IAAI,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,KACtB,OAAO,CAAC,IAAI,CAAC,CAAC;AAqBnB,wBAAgB,2BAA2B,CAC1C,OAAO,EAAE,qBAAqB,GAC5B,OAAO,CAwGT"}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mailgun inbound: a route that forwards to the application receives every
|
|
3
|
+
* mail as a form POST — the raw message in `body-mime`.
|
|
4
|
+
*
|
|
5
|
+
* router.post('/inbound/mailgun/mime', createMailgunInboundHandler({
|
|
6
|
+
* signingKey: env.get('MAILGUN_SIGNING_KEY'),
|
|
7
|
+
* async onMessage(message, { recipient }) { … },
|
|
8
|
+
* }))
|
|
9
|
+
*
|
|
10
|
+
* Configure the Mailgun route to forward to a URL ending in `mime`: that is
|
|
11
|
+
* what makes Mailgun send `body-mime`, the message as received.
|
|
12
|
+
*
|
|
13
|
+
* The fields are read from `ctx.request.body()`, which holds them only once
|
|
14
|
+
* Ream's body parser has read the form Mailgun posts (multipart/form-data or
|
|
15
|
+
* urlencoded): the route must run behind `BodyParserMiddleware`, with form
|
|
16
|
+
* and multipart limits above the largest message accepted. Without it every
|
|
17
|
+
* delivery is answered 401, its fields missing.
|
|
18
|
+
*
|
|
19
|
+
* Mailgun signs `timestamp` + `token` with the webhook signing key — and
|
|
20
|
+
* nothing else: the signature does not cover the message. So a signature is
|
|
21
|
+
* only worth what its freshness and uniqueness are worth, and both are
|
|
22
|
+
* enforced: a timestamp older than `maxAgeSeconds` is refused, and a token is
|
|
23
|
+
* handled once — claimed before handling, so two requests racing with it
|
|
24
|
+
* cannot both get through. A delivery whose handling failed gives its token
|
|
25
|
+
* back, so Mailgun's retry is handled; one already handled is acknowledged
|
|
26
|
+
* and not handled again.
|
|
27
|
+
*/
|
|
28
|
+
import { Buffer } from "node:buffer";
|
|
29
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
30
|
+
import { GoldstoneError } from "../errors.js";
|
|
31
|
+
import { readMessage, } from "../ReceivedMessage.js";
|
|
32
|
+
import { field, MemoryReplayGuard, } from "./context.js";
|
|
33
|
+
function expectedSignature(signingKey, timestamp, token) {
|
|
34
|
+
return createHmac("sha256", signingKey)
|
|
35
|
+
.update(timestamp + token)
|
|
36
|
+
.digest();
|
|
37
|
+
}
|
|
38
|
+
/** Constant time; a malformed hex string is simply not equal. */
|
|
39
|
+
function sameSignature(expected, given) {
|
|
40
|
+
if (!/^[0-9a-f]{64}$/i.test(given))
|
|
41
|
+
return false;
|
|
42
|
+
return timingSafeEqual(expected, Buffer.from(given, "hex"));
|
|
43
|
+
}
|
|
44
|
+
/** A day: a signature older than that is not a delivery, it is a replay. */
|
|
45
|
+
const MAX_AGE_LIMIT = 86_400;
|
|
46
|
+
export function createMailgunInboundHandler(options) {
|
|
47
|
+
if (!options.signingKey) {
|
|
48
|
+
throw new GoldstoneError("E_GOLDSTONE_INVALID_CONFIG", "createMailgunInboundHandler: signingKey is required to verify Mailgun's signature");
|
|
49
|
+
}
|
|
50
|
+
const { signingKey, onMessage } = options;
|
|
51
|
+
const maxAge = options.maxAgeSeconds ?? 120;
|
|
52
|
+
// The freshness window is a security switch: NaN or Infinity would turn
|
|
53
|
+
// the timestamp check and the replay guard off without a word.
|
|
54
|
+
if (!(Number.isInteger(maxAge) && maxAge >= 1 && maxAge <= MAX_AGE_LIMIT)) {
|
|
55
|
+
throw new GoldstoneError("E_GOLDSTONE_INVALID_CONFIG", `createMailgunInboundHandler: maxAgeSeconds must be an integer from 1 to ${MAX_AGE_LIMIT}, got ${String(maxAge)}`);
|
|
56
|
+
}
|
|
57
|
+
const guard = options.replayGuard ?? new MemoryReplayGuard();
|
|
58
|
+
return async (ctx) => {
|
|
59
|
+
const body = ctx.request.body();
|
|
60
|
+
const timestamp = field(body, "timestamp");
|
|
61
|
+
const token = field(body, "token");
|
|
62
|
+
const signature = field(body, "signature");
|
|
63
|
+
// One answer for every way a signature can be wrong, so the response
|
|
64
|
+
// does not say which check refused it.
|
|
65
|
+
const refuse = () => ctx.response.status(401).json({ error: "invalid_signature" });
|
|
66
|
+
if (!timestamp || !token || !signature)
|
|
67
|
+
return refuse();
|
|
68
|
+
const seconds = Number(timestamp);
|
|
69
|
+
if (!Number.isFinite(seconds) ||
|
|
70
|
+
Math.abs(Date.now() / 1000 - seconds) > maxAge) {
|
|
71
|
+
return refuse();
|
|
72
|
+
}
|
|
73
|
+
if (!sameSignature(expectedSignature(signingKey, timestamp, token), signature)) {
|
|
74
|
+
return refuse();
|
|
75
|
+
}
|
|
76
|
+
const mime = field(body, "body-mime");
|
|
77
|
+
if (mime === undefined) {
|
|
78
|
+
ctx.response.status(422).json({ error: "missing_body_mime" });
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
// Twice the window: a token older than that is refused on its
|
|
82
|
+
// timestamp anyway. Taken before handling, so a request racing with
|
|
83
|
+
// the same token is acknowledged instead of handled twice.
|
|
84
|
+
if (!(await guard.claim(token, maxAge * 2))) {
|
|
85
|
+
ctx.response.status(204).json({});
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
await handle(body, mime, ctx);
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
// Not handled: give the token back so Mailgun's retry gets through.
|
|
93
|
+
await guard.release(token);
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
async function handle(body, mime, ctx) {
|
|
98
|
+
let message;
|
|
99
|
+
try {
|
|
100
|
+
const authenticate = options.authenticate === false
|
|
101
|
+
? false
|
|
102
|
+
: {
|
|
103
|
+
...(options.authenticate === true ||
|
|
104
|
+
options.authenticate === undefined
|
|
105
|
+
? {}
|
|
106
|
+
: options.authenticate),
|
|
107
|
+
mailFrom: field(body, "sender"),
|
|
108
|
+
};
|
|
109
|
+
message = await readMessage(Buffer.from(mime, "utf8"), {
|
|
110
|
+
limits: options.limits,
|
|
111
|
+
authenticate,
|
|
112
|
+
remoteContent: options.remoteContent,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
if (error instanceof GoldstoneError) {
|
|
117
|
+
// 406 tells Mailgun not to retry: the same bytes would be refused
|
|
118
|
+
// again. The token stays taken.
|
|
119
|
+
ctx.response.status(406).json({ error: error.code });
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
// A handler that throws answers 500: Mailgun retries.
|
|
125
|
+
await onMessage(message, {
|
|
126
|
+
recipient: field(body, "recipient"),
|
|
127
|
+
sender: field(body, "sender"),
|
|
128
|
+
});
|
|
129
|
+
ctx.response.status(204).json({});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
//# sourceMappingURL=mailgun.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mailgun.js","sourceRoot":"","sources":["../../src/inbound/mailgun.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import "./augmentations.js";
|
|
2
|
+
export { type AuthenticateOptions, type Authenticity, type AuthResult, authenticate, type DkimVerdict, type DmarcPolicy, } from "./authenticity.js";
|
|
3
|
+
export { defineConfig, type InboxConfig, type MailboxConfig, type PasswordAuth, type TokenAuth, } from "./config.js";
|
|
4
|
+
export { fromEngine, GoldstoneError, GoldstoneNativeRequiredError, } from "./errors.js";
|
|
5
|
+
export { InboxManager } from "./InboxManager.js";
|
|
6
|
+
export { type InboundHttpContext, type InboundResponse, MemoryReplayGuard, type ReplayGuard, } from "./inbound/context.js";
|
|
7
|
+
export { createMailgunInboundHandler, type InboundMeta, type MailgunInboundOptions, } from "./inbound/mailgun.js";
|
|
8
|
+
export { Mailbox, type MailboxOptions, reconnectDelay, type SearchCriteria, type WatchOptions, } from "./Mailbox.js";
|
|
9
|
+
export { isNativeAvailable } from "./native.js";
|
|
10
|
+
export { type Attachment, type Header, type Limits, type Mailbox as Address, type ParseOptions, parseMessage, type ReadOptions, ReceivedMessage, readMessage, } from "./ReceivedMessage.js";
|
|
11
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,oBAAoB,CAAC;AAE5B,OAAO,EACN,KAAK,mBAAmB,EACxB,KAAK,YAAY,EACjB,KAAK,UAAU,EACf,YAAY,EACZ,KAAK,WAAW,EAChB,KAAK,WAAW,GAChB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACN,YAAY,EACZ,KAAK,WAAW,EAChB,KAAK,aAAa,EAClB,KAAK,YAAY,EACjB,KAAK,SAAS,GACd,MAAM,aAAa,CAAC;AACrB,OAAO,EACN,UAAU,EACV,cAAc,EACd,4BAA4B,GAC5B,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EACN,KAAK,kBAAkB,EACvB,KAAK,eAAe,EACpB,iBAAiB,EACjB,KAAK,WAAW,GAChB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACN,2BAA2B,EAC3B,KAAK,WAAW,EAChB,KAAK,qBAAqB,GAC1B,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACN,OAAO,EACP,KAAK,cAAc,EACnB,cAAc,EACd,KAAK,cAAc,EACnB,KAAK,YAAY,GACjB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EACN,KAAK,UAAU,EACf,KAAK,MAAM,EACX,KAAK,MAAM,EACX,KAAK,OAAO,IAAI,OAAO,EACvB,KAAK,YAAY,EACjB,YAAY,EACZ,KAAK,WAAW,EAChB,eAAe,EACf,WAAW,GACX,MAAM,sBAAsB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import "./augmentations.js";
|
|
2
|
+
export { authenticate, } from "./authenticity.js";
|
|
3
|
+
export { defineConfig, } from "./config.js";
|
|
4
|
+
export { fromEngine, GoldstoneError, GoldstoneNativeRequiredError, } from "./errors.js";
|
|
5
|
+
export { InboxManager } from "./InboxManager.js";
|
|
6
|
+
export { MemoryReplayGuard, } from "./inbound/context.js";
|
|
7
|
+
export { createMailgunInboundHandler, } from "./inbound/mailgun.js";
|
|
8
|
+
export { Mailbox, reconnectDelay, } from "./Mailbox.js";
|
|
9
|
+
export { isNativeAvailable } from "./native.js";
|
|
10
|
+
export { parseMessage, ReceivedMessage, readMessage, } from "./ReceivedMessage.js";
|
|
11
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,oBAAoB,CAAC;AAE5B,OAAO,EAIN,YAAY,GAGZ,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACN,YAAY,GAKZ,MAAM,aAAa,CAAC;AACrB,OAAO,EACN,UAAU,EACV,cAAc,EACd,4BAA4B,GAC5B,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAGN,iBAAiB,GAEjB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACN,2BAA2B,GAG3B,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACN,OAAO,EAEP,cAAc,GAGd,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EAMN,YAAY,EAEZ,eAAe,EACf,WAAW,GACX,MAAM,sBAAsB,CAAC"}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/** Bounds on one message; an absent field keeps the engine's default. */
|
|
2
|
+
export interface JsLimits {
|
|
3
|
+
maxBytes?: number;
|
|
4
|
+
maxParts?: number;
|
|
5
|
+
maxDepth?: number;
|
|
6
|
+
maxHeaders?: number;
|
|
7
|
+
maxHeaderBytes?: number;
|
|
8
|
+
maxAttachments?: number;
|
|
9
|
+
maxAttachmentBytes?: number;
|
|
10
|
+
maxHtmlBytes?: number;
|
|
11
|
+
}
|
|
12
|
+
/** What the sanitized HTML may load by itself. */
|
|
13
|
+
export interface JsHtmlOptions {
|
|
14
|
+
/**
|
|
15
|
+
* Keep remote images and other URLs a browser loads unasked. Default
|
|
16
|
+
* false.
|
|
17
|
+
*/
|
|
18
|
+
remoteContent?: boolean;
|
|
19
|
+
}
|
|
20
|
+
export interface JsMailbox {
|
|
21
|
+
name?: string;
|
|
22
|
+
address?: string;
|
|
23
|
+
}
|
|
24
|
+
export interface JsHeader {
|
|
25
|
+
name: string;
|
|
26
|
+
value: string;
|
|
27
|
+
}
|
|
28
|
+
export interface JsAttachment {
|
|
29
|
+
fileName: string;
|
|
30
|
+
originalName?: string;
|
|
31
|
+
contentType: string;
|
|
32
|
+
content: Buffer;
|
|
33
|
+
contentId?: string;
|
|
34
|
+
inline: boolean;
|
|
35
|
+
}
|
|
36
|
+
export interface JsMessage {
|
|
37
|
+
messageId?: string;
|
|
38
|
+
subject?: string;
|
|
39
|
+
date?: string;
|
|
40
|
+
from: Array<JsMailbox>;
|
|
41
|
+
to: Array<JsMailbox>;
|
|
42
|
+
cc: Array<JsMailbox>;
|
|
43
|
+
bcc: Array<JsMailbox>;
|
|
44
|
+
replyTo: Array<JsMailbox>;
|
|
45
|
+
inReplyTo: Array<string>;
|
|
46
|
+
references: Array<string>;
|
|
47
|
+
headers: Array<JsHeader>;
|
|
48
|
+
text?: string;
|
|
49
|
+
html?: string;
|
|
50
|
+
/** `E_GOLDSTONE_HTML_*: …` when the HTML body was not sanitized. */
|
|
51
|
+
htmlError?: string;
|
|
52
|
+
unsanitizedHtml?: string;
|
|
53
|
+
attachments: Array<JsAttachment>;
|
|
54
|
+
}
|
|
55
|
+
/** The envelope SPF checks, and the bounds on the whole check. */
|
|
56
|
+
export interface JsAuthOptions {
|
|
57
|
+
ip?: string;
|
|
58
|
+
helo?: string;
|
|
59
|
+
mailFrom?: string;
|
|
60
|
+
maxDkimSignatures?: number;
|
|
61
|
+
timeoutMs?: number;
|
|
62
|
+
maxBytes?: number;
|
|
63
|
+
}
|
|
64
|
+
export interface JsDkimVerdict {
|
|
65
|
+
result: string;
|
|
66
|
+
domain?: string;
|
|
67
|
+
selector?: string;
|
|
68
|
+
reason?: string;
|
|
69
|
+
}
|
|
70
|
+
export interface JsVerdict {
|
|
71
|
+
dkim: Array<JsDkimVerdict>;
|
|
72
|
+
spf: string;
|
|
73
|
+
dmarc: string;
|
|
74
|
+
dmarcPolicy: string;
|
|
75
|
+
dmarcDomain?: string;
|
|
76
|
+
fromDomain?: string;
|
|
77
|
+
arc: string;
|
|
78
|
+
summary: string;
|
|
79
|
+
}
|
|
80
|
+
export interface JsReadMessage {
|
|
81
|
+
message: JsMessage;
|
|
82
|
+
authenticity: JsVerdict;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Parse one received message, off the JavaScript thread: parsing and
|
|
86
|
+
* sanitizing are CPU work on bytes a stranger sent. Rejects with
|
|
87
|
+
* `E_GOLDSTONE_*: …` on a message that breaks a limit or is not a message.
|
|
88
|
+
*
|
|
89
|
+
* `raw`, here and in the two calls below, is read on a runtime thread while
|
|
90
|
+
* JavaScript runs on: it must be memory nothing writes to meanwhile, which
|
|
91
|
+
* the TypeScript side ensures by handing a copy (`snapshot` in native.ts).
|
|
92
|
+
* A copy made here would be made on the runtime thread, too late.
|
|
93
|
+
*/
|
|
94
|
+
export declare function parseMessage(raw: Buffer, limits?: JsLimits | undefined | null, html?: JsHtmlOptions | undefined | null): Promise<JsMessage>;
|
|
95
|
+
/**
|
|
96
|
+
* DKIM, SPF (when the envelope is given), DMARC and ARC for one received
|
|
97
|
+
* message. Resolves with `temperror` verdicts when DNS cannot answer in
|
|
98
|
+
* time — never with a pass.
|
|
99
|
+
*/
|
|
100
|
+
export declare function authenticateMessage(raw: Buffer, options?: JsAuthOptions | undefined | null): Promise<JsVerdict>;
|
|
101
|
+
/**
|
|
102
|
+
* Parse one received message and check who sent it, from the same parse:
|
|
103
|
+
* the From the verdict speaks for is the From the message shows.
|
|
104
|
+
*/
|
|
105
|
+
export declare function readMessage(raw: Buffer, limits?: JsLimits | undefined | null, options?: JsAuthOptions | undefined | null, html?: JsHtmlOptions | undefined | null): Promise<JsReadMessage>;
|
|
106
|
+
//# sourceMappingURL=generated.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generated.d.ts","sourceRoot":"","sources":["../../src/native/generated.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// GENERATED FROM THE RUST — do not edit.
|
|
2
|
+
//
|
|
3
|
+
// Produced by scripts/generate-napi-types.mjs from napi-derive's type-def
|
|
4
|
+
// output. Editing this file by hand puts it back where it started: a
|
|
5
|
+
// description that can disagree with the code it describes.
|
|
6
|
+
export {};
|
|
7
|
+
//# sourceMappingURL=generated.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generated.js","sourceRoot":"","sources":["../../src/native/generated.ts"],"names":[],"mappings":"AAAA,yCAAyC;AACzC,EAAE;AACF,0EAA0E;AAC1E,qEAAqE;AACrE,4DAA4D"}
|
package/dist/native.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loader for the Rust mail engine.
|
|
3
|
+
*
|
|
4
|
+
* The engine is not optional: received mail is hostile input, and its parser
|
|
5
|
+
* is the one place this package will not fall back to JavaScript. A missing
|
|
6
|
+
* binary is a hard failure with an actionable message.
|
|
7
|
+
*/
|
|
8
|
+
/** What this package calls, as the Rust declares it (`pnpm build:napi-types`). */
|
|
9
|
+
declare const NATIVE_CALLS: readonly ["parseMessage", "readMessage", "authenticateMessage"];
|
|
10
|
+
type NativeGoldstone = Pick<typeof import("./native/generated.js"), (typeof NATIVE_CALLS)[number]>;
|
|
11
|
+
export declare function isNativeAvailable(): boolean;
|
|
12
|
+
/**
|
|
13
|
+
* The bytes handed to the engine: a copy, taken here on the JavaScript
|
|
14
|
+
* thread. The engine reads them later, off this thread, more than once (the
|
|
15
|
+
* walk that bounds the structure, then the parse); on a view of the caller's
|
|
16
|
+
* memory, a buffer reused or written meanwhile would make it bound one
|
|
17
|
+
* message and parse another.
|
|
18
|
+
*/
|
|
19
|
+
export declare function snapshot(raw: Uint8Array): Buffer;
|
|
20
|
+
/** The engine, or a refusal explaining how to get one. */
|
|
21
|
+
export declare function engine(): NativeGoldstone;
|
|
22
|
+
export {};
|
|
23
|
+
//# sourceMappingURL=native.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"native.d.ts","sourceRoot":"","sources":["../src/native.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AASH,kFAAkF;AAClF,QAAA,MAAM,YAAY,iEAIR,CAAC;AAEX,KAAK,eAAe,GAAG,IAAI,CAC1B,cAAc,uBAAuB,CAAC,EACtC,CAAC,OAAO,YAAY,CAAC,CAAC,MAAM,CAAC,CAC7B,CAAC;AAWF,wBAAgB,iBAAiB,IAAI,OAAO,CAE3C;AAED;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,UAAU,GAAG,MAAM,CAEhD;AAED,0DAA0D;AAC1D,wBAAgB,MAAM,IAAI,eAAe,CASxC"}
|
package/dist/native.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loader for the Rust mail engine.
|
|
3
|
+
*
|
|
4
|
+
* The engine is not optional: received mail is hostile input, and its parser
|
|
5
|
+
* is the one place this package will not fall back to JavaScript. A missing
|
|
6
|
+
* binary is a hard failure with an actionable message.
|
|
7
|
+
*/
|
|
8
|
+
import { GoldstoneNativeRequiredError } from "./errors.js";
|
|
9
|
+
import { hasFunctions, loadNativeBinary, unavailableReason as vendorUnavailableReason, } from "./vendor/nativeBinary.js";
|
|
10
|
+
/** What this package calls, as the Rust declares it (`pnpm build:napi-types`). */
|
|
11
|
+
const NATIVE_CALLS = [
|
|
12
|
+
"parseMessage",
|
|
13
|
+
"readMessage",
|
|
14
|
+
"authenticateMessage",
|
|
15
|
+
];
|
|
16
|
+
/** The same three calls, checked at load: a stale binary loads just as well. */
|
|
17
|
+
function isNativeGoldstone(value) {
|
|
18
|
+
return hasFunctions(value, NATIVE_CALLS);
|
|
19
|
+
}
|
|
20
|
+
const attempt = loadNativeBinary(isNativeGoldstone, NATIVE_CALLS);
|
|
21
|
+
const native = attempt.loaded ? attempt.binary : undefined;
|
|
22
|
+
const loadError = attempt.loaded ? undefined : attempt.cause;
|
|
23
|
+
export function isNativeAvailable() {
|
|
24
|
+
return native !== undefined;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The bytes handed to the engine: a copy, taken here on the JavaScript
|
|
28
|
+
* thread. The engine reads them later, off this thread, more than once (the
|
|
29
|
+
* walk that bounds the structure, then the parse); on a view of the caller's
|
|
30
|
+
* memory, a buffer reused or written meanwhile would make it bound one
|
|
31
|
+
* message and parse another.
|
|
32
|
+
*/
|
|
33
|
+
export function snapshot(raw) {
|
|
34
|
+
return Buffer.from(raw);
|
|
35
|
+
}
|
|
36
|
+
/** The engine, or a refusal explaining how to get one. */
|
|
37
|
+
export function engine() {
|
|
38
|
+
if (native === undefined) {
|
|
39
|
+
throw new GoldstoneNativeRequiredError(loadError === undefined
|
|
40
|
+
? vendorUnavailableReason()
|
|
41
|
+
: `failed to load (${loadError instanceof Error ? loadError.message : String(loadError)})`);
|
|
42
|
+
}
|
|
43
|
+
return native;
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=native.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"native.js","sourceRoot":"","sources":["../src/native.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,4BAA4B,EAAE,MAAM,aAAa,CAAC;AAC3D,OAAO,EACN,YAAY,EACZ,gBAAgB,EAChB,iBAAiB,IAAI,uBAAuB,GAC5C,MAAM,0BAA0B,CAAC;AAElC,kFAAkF;AAClF,MAAM,YAAY,GAAG;IACpB,cAAc;IACd,aAAa;IACb,qBAAqB;CACZ,CAAC;AAOX,gFAAgF;AAChF,SAAS,iBAAiB,CAAC,KAAc;IACxC,OAAO,YAAY,CAAC,KAAK,EAAE,YAAY,CAAC,CAAC;AAC1C,CAAC;AAED,MAAM,OAAO,GAAG,gBAAgB,CAAC,iBAAiB,EAAE,YAAY,CAAC,CAAC;AAClE,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC;AAC3D,MAAM,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC;AAE7D,MAAM,UAAU,iBAAiB;IAChC,OAAO,MAAM,KAAK,SAAS,CAAC;AAC7B,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,QAAQ,CAAC,GAAe;IACvC,OAAO,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACzB,CAAC;AAED,0DAA0D;AAC1D,MAAM,UAAU,MAAM;IACrB,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QAC1B,MAAM,IAAI,4BAA4B,CACrC,SAAS,KAAK,SAAS;YACtB,CAAC,CAAC,uBAAuB,EAAE;YAC3B,CAAC,CAAC,mBAAmB,SAAS,YAAY,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,GAAG,CAC3F,CAAC;IACH,CAAC;IACD,OAAO,MAAM,CAAC;AACf,CAAC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The {@link InboxManager}, reachable without threading it through every
|
|
3
|
+
* call site — mirroring `@c9up/quasar/services/main`.
|
|
4
|
+
*
|
|
5
|
+
* import inbox from "@c9up/goldstone/services/main"
|
|
6
|
+
*
|
|
7
|
+
* const unread = await inbox.mailbox().search({ seen: false })
|
|
8
|
+
*
|
|
9
|
+
* Importing this module loads nothing: the instance is resolved on the first
|
|
10
|
+
* member access, so a unit test that pulls it in transitively needs no booted
|
|
11
|
+
* provider.
|
|
12
|
+
*/
|
|
13
|
+
import type { InboxManager } from "../InboxManager.js";
|
|
14
|
+
/** Seat the instance — called by GoldstoneProvider, or by an app wiring its own. */
|
|
15
|
+
export declare function setInbox(inbox: InboxManager): void;
|
|
16
|
+
/** The seated instance, if there is one. */
|
|
17
|
+
export declare function getInbox(): InboxManager | undefined;
|
|
18
|
+
/** Drop the instance IF it is still the one passed in (ownership guard). */
|
|
19
|
+
export declare function clearInbox(inbox: InboxManager): void;
|
|
20
|
+
declare const inbox: InboxManager;
|
|
21
|
+
export default inbox;
|
|
22
|
+
//# sourceMappingURL=main.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../src/services/main.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAIvD,oFAAoF;AACpF,wBAAgB,QAAQ,CAAC,KAAK,EAAE,YAAY,GAAG,IAAI,CAElD;AAED,4CAA4C;AAC5C,wBAAgB,QAAQ,IAAI,YAAY,GAAG,SAAS,CAEnD;AAED,4EAA4E;AAC5E,wBAAgB,UAAU,CAAC,KAAK,EAAE,YAAY,GAAG,IAAI,CAEpD;AAED,QAAA,MAAM,KAAK,EAAE,YAgBX,CAAC;AAEH,eAAe,KAAK,CAAC"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The {@link InboxManager}, reachable without threading it through every
|
|
3
|
+
* call site — mirroring `@c9up/quasar/services/main`.
|
|
4
|
+
*
|
|
5
|
+
* import inbox from "@c9up/goldstone/services/main"
|
|
6
|
+
*
|
|
7
|
+
* const unread = await inbox.mailbox().search({ seen: false })
|
|
8
|
+
*
|
|
9
|
+
* Importing this module loads nothing: the instance is resolved on the first
|
|
10
|
+
* member access, so a unit test that pulls it in transitively needs no booted
|
|
11
|
+
* provider.
|
|
12
|
+
*/
|
|
13
|
+
let instance;
|
|
14
|
+
/** Seat the instance — called by GoldstoneProvider, or by an app wiring its own. */
|
|
15
|
+
export function setInbox(inbox) {
|
|
16
|
+
instance = inbox;
|
|
17
|
+
}
|
|
18
|
+
/** The seated instance, if there is one. */
|
|
19
|
+
export function getInbox() {
|
|
20
|
+
return instance;
|
|
21
|
+
}
|
|
22
|
+
/** Drop the instance IF it is still the one passed in (ownership guard). */
|
|
23
|
+
export function clearInbox(inbox) {
|
|
24
|
+
if (instance === inbox)
|
|
25
|
+
instance = undefined;
|
|
26
|
+
}
|
|
27
|
+
const inbox = new Proxy(Object.create(null), {
|
|
28
|
+
get(_target, property) {
|
|
29
|
+
// A module loader reads `then` and various symbols off what it imports
|
|
30
|
+
// before anyone uses it. They are not members, so answer undefined and
|
|
31
|
+
// let a genuine access be the one that reports.
|
|
32
|
+
if (typeof property === "symbol" || property === "then") {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
if (!instance) {
|
|
36
|
+
throw new Error("[inbox] accessed before initialization — register GoldstoneProvider, or call setInbox() yourself.");
|
|
37
|
+
}
|
|
38
|
+
const value = Reflect.get(instance, property, instance);
|
|
39
|
+
return typeof value === "function" ? value.bind(instance) : value;
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
export default inbox;
|
|
43
|
+
//# sourceMappingURL=main.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"main.js","sourceRoot":"","sources":["../../src/services/main.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH,IAAI,QAAkC,CAAC;AAEvC,oFAAoF;AACpF,MAAM,UAAU,QAAQ,CAAC,KAAmB;IAC3C,QAAQ,GAAG,KAAK,CAAC;AAClB,CAAC;AAED,4CAA4C;AAC5C,MAAM,UAAU,QAAQ;IACvB,OAAO,QAAQ,CAAC;AACjB,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,UAAU,CAAC,KAAmB;IAC7C,IAAI,QAAQ,KAAK,KAAK;QAAE,QAAQ,GAAG,SAAS,CAAC;AAC9C,CAAC;AAED,MAAM,KAAK,GAAiB,IAAI,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE;IAC1D,GAAG,CAAC,OAAO,EAAE,QAAQ;QACpB,uEAAuE;QACvE,uEAAuE;QACvE,gDAAgD;QAChD,IAAI,OAAO,QAAQ,KAAK,QAAQ,IAAI,QAAQ,KAAK,MAAM,EAAE,CAAC;YACzD,OAAO,SAAS,CAAC;QAClB,CAAC;QACD,IAAI,CAAC,QAAQ,EAAE,CAAC;YACf,MAAM,IAAI,KAAK,CACd,mGAAmG,CACnG,CAAC;QACH,CAAC;QACD,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAC;QACxD,OAAO,OAAO,KAAK,KAAK,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;IACnE,CAAC;CACD,CAAC,CAAC;AAEH,eAAe,KAAK,CAAC"}
|