@wtfalch/email 0.1.1 → 0.4.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.
@@ -31,13 +31,11 @@ export type MailClientOptions = {
31
31
  /** Use a different account than the session's primary one. */
32
32
  accountId?: string;
33
33
  };
34
- /**
35
- * A connection to one JMAP account.
36
- *
37
- * Every other module in this package takes one of these. It owns the session
38
- * document, the account id, and the `jmap-jam` client the wire calls go
39
- * through; `jmap-jam` itself is not part of this package's public surface.
40
- */
34
+ /** Passed to every `jmap-jam` call, whose `fetchInit` is spread over the
35
+ * request it builds. */
36
+ export declare const JAM_FETCH: {
37
+ fetchInit: RequestInit;
38
+ };
41
39
  export declare class MailClient {
42
40
  #private;
43
41
  constructor(options: MailClientOptions);
@@ -1,5 +1,5 @@
1
1
  import { JamClient } from 'jmap-jam';
2
- import { MailError, guard, toMailError } from "./errors.js";
2
+ import { MailError, guard } from "./errors.js";
3
3
  import { expandTemplate } from "./uri.js";
4
4
  export const MAIL_CAPABILITY = 'urn:ietf:params:jmap:mail';
5
5
  export const SUBMISSION_CAPABILITY = 'urn:ietf:params:jmap:submission';
@@ -16,6 +16,26 @@ function looksLikeSession(value) {
16
16
  * document, the account id, and the `jmap-jam` client the wire calls go
17
17
  * through; `jmap-jam` itself is not part of this package's public surface.
18
18
  */
19
+ /**
20
+ * Never let the browser answer an authentication challenge for us.
21
+ *
22
+ * Every request here carries its own `Authorization` header. If one is
23
+ * refused, Stalwart answers with both `WWW-Authenticate: Bearer` and
24
+ * `WWW-Authenticate: Basic`, and a browser that is allowed to use
25
+ * credentials honours the second by opening its native password prompt —
26
+ * a modal that hides whatever the application was about to say about the
27
+ * refusal. `omit` declines that on our behalf: the 401 comes back as an
28
+ * ordinary response and the application gets to explain it.
29
+ *
30
+ * Outside a browser this is inert.
31
+ */
32
+ const NO_BROWSER_CREDENTIALS = { credentials: 'omit' };
33
+ /** Passed to every `jmap-jam` call, whose `fetchInit` is spread over the
34
+ * request it builds. */
35
+ export const JAM_FETCH = { fetchInit: NO_BROWSER_CREDENTIALS };
36
+ /** Handed to `JamClient`'s constructor so its eager session request resolves
37
+ * in-process instead of going to the server. See `#open`. */
38
+ const NO_SESSION_URL = 'data:application/json,{}';
19
39
  export class MailClient {
20
40
  #sessionUrl;
21
41
  #wantedAccountId;
@@ -60,37 +80,34 @@ export class MailClient {
60
80
  async #open() {
61
81
  const authorization = this.#authorization;
62
82
  const isBearer = authorization.startsWith('Bearer ');
63
- // `JamClient` can only build a `Bearer` header from the string it is
64
- // given, so a credential that is not a bearer token (an app password,
65
- // which Stalwart takes as Basic) needs the header replaced afterwards.
66
- // The constructor has already fired its own session request by then, so
67
- // that path reads the session here and discards what the constructor
68
- // fetched; the bearer path, which is what the product uses, costs one
69
- // request as usual.
83
+ // `JamClient`'s constructor fires a session request of its own, and we
84
+ // want neither the request nor its result. It calls `.json()` without
85
+ // looking at the status, so a refusal arrives as an unreadable parse
86
+ // error and, worse, it is a plain same-origin fetch, so a 401 carrying
87
+ // `WWW-Authenticate: Basic` makes the browser put up its own password
88
+ // prompt. To a person that is indistinguishable from being asked to sign
89
+ // in again, on the right domain, when in fact they are already signed in
90
+ // and the token is the problem.
91
+ //
92
+ // So point the constructor at a `data:` URL, which resolves in-process
93
+ // and never touches the network, and read the session ourselves with the
94
+ // status checked. `jam.session` is overwritten with that document, and
95
+ // it is the document — not the URL — that carries `apiUrl`, so nothing
96
+ // downstream notices.
70
97
  const jam = new JamClient({
71
- sessionUrl: this.#sessionUrl,
98
+ sessionUrl: NO_SESSION_URL,
72
99
  bearerToken: isBearer ? authorization.slice('Bearer '.length) : '',
73
100
  });
74
- let doc;
75
- if (isBearer) {
76
- let loaded;
77
- try {
78
- loaded = await jam.session;
79
- }
80
- catch (cause) {
81
- // `JamClient.loadSession` calls `.json()` without checking the
82
- // status, so a 401 with an HTML body arrives here as a parse error.
83
- throw await this.#sessionFailure(authorization, cause);
84
- }
85
- if (!looksLikeSession(loaded))
86
- throw await this.#sessionFailure(authorization, loaded);
87
- doc = loaded;
88
- }
89
- else {
101
+ // Nothing ever reads the promise the constructor made, and in Node an
102
+ // unobserved rejection takes the process down. Say so explicitly.
103
+ void jam.session.catch(() => { });
104
+ // `JamClient` can only build a `Bearer` header from the string it is
105
+ // given, so a credential that is not a bearer token — an app password,
106
+ // which Stalwart takes as Basic — needs the header replaced.
107
+ if (!isBearer)
90
108
  jam.authHeader = authorization;
91
- doc = await this.#fetchSession(authorization);
92
- jam.session = Promise.resolve(doc);
93
- }
109
+ const doc = await this.#fetchSession(authorization);
110
+ jam.session = Promise.resolve(doc);
94
111
  const accountId = this.#wantedAccountId ?? doc.primaryAccounts?.[MAIL_CAPABILITY];
95
112
  if (!accountId) {
96
113
  throw new MailError(`the session document names no primary account for ${MAIL_CAPABILITY}; the server may not offer JMAP Mail to this login`, { operation: 'client.connect' });
@@ -110,6 +127,7 @@ export class MailClient {
110
127
  response = await fetch(this.#sessionUrl, {
111
128
  headers: { Authorization: authorization, Accept: 'application/json' },
112
129
  cache: 'no-cache',
130
+ ...NO_BROWSER_CREDENTIALS,
113
131
  });
114
132
  }
115
133
  catch (cause) {
@@ -134,18 +152,6 @@ export class MailClient {
134
152
  }
135
153
  return parsed;
136
154
  }
137
- /** Turn a failed or unrecognisable session read into an error that says
138
- * what happened, by asking again with the status checked. */
139
- async #sessionFailure(authorization, cause) {
140
- try {
141
- await this.#fetchSession(authorization);
142
- }
143
- catch (error) {
144
- return toMailError(error, 'client.connect');
145
- }
146
- // The second read succeeded, so the first was a transient failure.
147
- return toMailError(cause, 'client.connect');
148
- }
149
155
  /** The signed-in person and the account being read. */
150
156
  async session() {
151
157
  const { doc, accountId } = await this.connect();
@@ -185,6 +191,7 @@ export class MailClient {
185
191
  return guard('client.upload', async () => {
186
192
  const result = await jam.uploadBlob(accountId, body, {
187
193
  headers: { Authorization: this.#authorization, 'Content-Type': type },
194
+ ...NO_BROWSER_CREDENTIALS,
188
195
  });
189
196
  return { blobId: result.blobId, type: result.type, size: result.size };
190
197
  });
@@ -1,4 +1,4 @@
1
- import type { MailClient } from './client.ts';
1
+ import { type MailClient } from './client.ts';
2
2
  import type { MailIdentity } from './types.ts';
3
3
  /** Every address this account may send as. */
4
4
  export declare function identities(client: MailClient): Promise<readonly MailIdentity[]>;
@@ -1,9 +1,10 @@
1
+ import { JAM_FETCH } from "./client.js";
1
2
  import { guard } from "./errors.js";
2
3
  /** Every address this account may send as. */
3
4
  export async function identities(client) {
4
5
  const { jam, accountId } = await client.connect();
5
6
  return guard('identities', async () => {
6
- const [result] = await jam.api.Identity.get({ accountId });
7
+ const [result] = await jam.api.Identity.get({ accountId }, JAM_FETCH);
7
8
  return result.list.map((identity) => ({
8
9
  id: identity.id,
9
10
  name: identity.name ?? '',
@@ -1,4 +1,4 @@
1
- import type { MailClient } from './client.ts';
1
+ import { type MailClient } from './client.ts';
2
2
  import type { MailboxNode, MailboxRole } from './types.ts';
3
3
  /** A mailbox as the server returns it, before the tree is built. */
4
4
  export type FlatMailbox = {
@@ -1,3 +1,4 @@
1
+ import { JAM_FETCH } from "./client.js";
1
2
  import { guard } from "./errors.js";
2
3
  /** RFC 8621 §2: order by `sortOrder`, and break ties by name. */
3
4
  function compare(a, b) {
@@ -101,7 +102,7 @@ export async function mailboxes(client) {
101
102
  const [result] = await jam.api.Mailbox.get({
102
103
  accountId,
103
104
  properties: MAILBOX_PROPERTIES,
104
- });
105
+ }, JAM_FETCH);
105
106
  return buildTree(result.list);
106
107
  });
107
108
  }
@@ -1,8 +1,52 @@
1
1
  import type { MailClient } from '../client.ts';
2
+ import type { MailError } from '../errors.ts';
3
+ /**
4
+ * Where the reader is: which mailbox, which conversation.
5
+ *
6
+ * Its own type because an application that puts this in the URL has to be
7
+ * able to name it.
8
+ */
9
+ export type MailLocation = {
10
+ mailboxId: string | null;
11
+ threadId: string | null;
12
+ };
2
13
  export type MailProps = {
3
14
  client: MailClient;
15
+ /**
16
+ * Drive the selection from outside — from the URL, almost always.
17
+ *
18
+ * Omit it and the component keeps its own, which is right for a gallery or
19
+ * an embed. Supply it with `onNavigate` and the two selections become the
20
+ * application's: `/inbox` and `/thread/abc` can be real addresses that
21
+ * survive a reload, appear in history and can be sent to someone.
22
+ *
23
+ * A partially controlled component is a trap, so this is all or nothing:
24
+ * pass `location` and you own both fields.
25
+ */
26
+ location?: MailLocation;
27
+ /** Called when something in here wants to move. Required with `location`,
28
+ * because without it the controlled selection could never change. */
29
+ onNavigate?: (to: MailLocation) => void;
30
+ /**
31
+ * The right-hand end of the header: who is signed in, and the way out.
32
+ *
33
+ * A slot rather than anything this component builds, because `Mail` does
34
+ * not know how the person got here. It has no idea whether there is a
35
+ * session, a token, an issuer or a sign-out — and a mail view that grew
36
+ * opinions about authentication would be a mail view nobody could embed.
37
+ */
38
+ account?: React.ReactNode;
39
+ /**
40
+ * Called when any of the fetches behind this view fails.
41
+ *
42
+ * The views already say "could not load" for themselves; this is for the
43
+ * application to act on a *class* of failure it alone can answer. The one
44
+ * that matters is 401: the token expired mid-session, and only the thing
45
+ * that obtained it can get another.
46
+ */
47
+ onError?: (error: MailError) => void;
4
48
  /** Now, for the list's relative times. Passed in so stories hold still. */
5
49
  now?: Date;
6
50
  className?: string;
7
51
  };
8
- export declare function Mail({ client, now, className }: MailProps): import("react").JSX.Element;
52
+ export declare function Mail({ client, location, onNavigate, account, onError, now, className, }: MailProps): import("react").JSX.Element;
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Button, Command, Icon, Input, Modal, SplitPane } from '@wtfalch/design';
3
- import { useCallback, useEffect, useMemo, useState } from 'react';
3
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
4
4
  import { forwardDraft, replyDraft } from "../drafts.js";
5
5
  import { findRole } from "../mailboxes.js";
6
6
  import { send } from "../submit.js";
@@ -27,9 +27,25 @@ import { useIdentities, useMailboxes, usePush, useThread, useThreads } from "./h
27
27
  * mailboxes as "go to" entries, so moving around never requires the mouse.
28
28
  */
29
29
  const PAGE = 50;
30
- export function Mail({ client, now, className }) {
31
- const [mailboxId, setMailboxId] = useState(null);
32
- const [threadId, setThreadId] = useState(null);
30
+ export function Mail({ client, location, onNavigate, account, onError, now, className, }) {
31
+ const [ownMailboxId, setOwnMailboxId] = useState(null);
32
+ const [ownThreadId, setOwnThreadId] = useState(null);
33
+ /* Controlled when a location was given, and then entirely: reading one
34
+ field from the caller and the other from here is how a component ends up
35
+ with two disagreeing ideas of where it is. */
36
+ const controlled = location !== undefined;
37
+ const mailboxId = controlled ? location.mailboxId : ownMailboxId;
38
+ const threadId = controlled ? location.threadId : ownThreadId;
39
+ const navigate = useCallback((to) => {
40
+ if (controlled)
41
+ onNavigate?.(to);
42
+ else {
43
+ setOwnMailboxId(to.mailboxId);
44
+ setOwnThreadId(to.threadId);
45
+ }
46
+ }, [controlled, onNavigate]);
47
+ const setMailboxId = useCallback((next) => navigate({ mailboxId: next, threadId: null }), [navigate]);
48
+ const setThreadId = useCallback((next) => navigate({ mailboxId, threadId: next }), [navigate, mailboxId]);
33
49
  const [position, setPosition] = useState(0);
34
50
  const [query, setQuery] = useState('');
35
51
  const [text, setText] = useState('');
@@ -48,7 +64,7 @@ export function Mail({ client, now, className }) {
48
64
  const inbox = findRole(boxes.data, 'inbox') ?? boxes.data[0];
49
65
  if (inbox)
50
66
  setMailboxId(inbox.id);
51
- }, [boxes.data, mailboxId]);
67
+ }, [boxes.data, mailboxId, setMailboxId]);
52
68
  const threads = useThreads({ mailboxId: mailboxId ?? undefined, text: query, position, limit: PAGE, calculateTotal: true }, client);
53
69
  const thread = useThread(threadId, client);
54
70
  const refresh = useCallback(() => {
@@ -56,14 +72,24 @@ export function Mail({ client, now, className }) {
56
72
  threads.reload();
57
73
  }, [boxes.reload, threads.reload]);
58
74
  usePush(refresh, client);
75
+ /* Errors reach the application, but only when they change: these hooks hold
76
+ their last error while a retry is in flight, so forwarding on every
77
+ render would call `onError` sixty times for one dead token. */
78
+ const failure = boxes.error ?? threads.error ?? thread.error ?? identities.error;
79
+ const reported = useRef(undefined);
80
+ useEffect(() => {
81
+ if (!failure || reported.current === failure)
82
+ return;
83
+ reported.current = failure;
84
+ onError?.(failure);
85
+ }, [failure, onError]);
59
86
  const go = useCallback((mailbox) => {
60
- setMailboxId(mailbox.id);
61
- setThreadId(null);
87
+ navigate({ mailboxId: mailbox.id, threadId: null });
62
88
  setPosition(0);
63
89
  setQuery('');
64
90
  setText('');
65
- }, []);
66
- const openThread = useCallback((summary) => setThreadId(summary.id), []);
91
+ }, [navigate]);
92
+ const openThread = useCallback((summary) => setThreadId(summary.id), [setThreadId]);
67
93
  /* Cmd-K, and Escape out of a search. Bound on the window rather than on a
68
94
  container, because the palette has to open from wherever focus is --
69
95
  including from inside the message being read. */
@@ -145,5 +171,5 @@ export function Mail({ client, now, className }) {
145
171
  return;
146
172
  setText('');
147
173
  setQuery('');
148
- } }) }), _jsx(Button, { kind: "ghost", onClick: () => setPaletteOpen(true), "aria-label": "Open the palette", children: _jsx(Icon, { name: "bolt" }) })] }), _jsxs(SplitPane, { className: "mail-panes", label: "Mailbox list width", defaultSize: 22, min: 14, max: 40, storageKey: "mail-sidebar", children: [_jsx(MailboxTree, { mailboxes: boxes.data ?? [], selectedId: mailboxId, onSelect: go, loading: boxes.loading }), _jsxs(SplitPane, { label: "Conversation list width", defaultSize: 38, min: 22, max: 60, storageKey: "mail-list", children: [_jsx(ThreadList, { page: threads.data, selectedId: threadId, onSelect: openThread, onPage: setPosition, limit: PAGE, loading: threads.loading, now: now, empty: query ? _jsxs("p", { className: "threads-quiet", children: ["Nothing matches \u201C", query, "\u201D."] }) : undefined }), _jsx(ThreadView, { thread: thread.data, loading: thread.loading, onReply: startReply, onForward: startForward })] })] }), _jsx(Command, { open: paletteOpen, onOpenChange: setPaletteOpen, groups: paletteGroups }), draft && (_jsx(Modal, { title: "New message", width: "46rem", onClose: sending ? undefined : () => setDraft(null), closeDisabled: sending, children: _jsx(Composer, { draft: draft, onChange: setDraft, onSend: doSend, onCancel: () => setDraft(null), identities: identities.data ?? [], sending: sending, error: sendError }) }))] }));
174
+ } }) }), _jsx(Button, { kind: "ghost", onClick: () => setPaletteOpen(true), "aria-label": "Open the palette", children: _jsx(Icon, { name: "bolt" }) }), account] }), _jsxs(SplitPane, { className: "mail-panes", label: "Mailbox list width", defaultSize: 22, min: 14, max: 40, storageKey: "mail-sidebar", children: [_jsx(MailboxTree, { mailboxes: boxes.data ?? [], selectedId: mailboxId, onSelect: go, loading: boxes.loading }), _jsxs(SplitPane, { label: "Conversation list width", defaultSize: 38, min: 22, max: 60, storageKey: "mail-list", children: [_jsx(ThreadList, { page: threads.data, selectedId: threadId, onSelect: openThread, onPage: setPosition, limit: PAGE, loading: threads.loading, now: now, empty: query ? _jsxs("p", { className: "threads-quiet", children: ["Nothing matches \u201C", query, "\u201D."] }) : undefined }), _jsx(ThreadView, { thread: thread.data, loading: thread.loading, onReply: startReply, onForward: startForward })] })] }), _jsx(Command, { open: paletteOpen, onOpenChange: setPaletteOpen, groups: paletteGroups }), draft && (_jsx(Modal, { title: "New message", width: "46rem", onClose: sending ? undefined : () => setDraft(null), closeDisabled: sending, children: _jsx(Composer, { draft: draft, onChange: setDraft, onSend: doSend, onCancel: () => setDraft(null), identities: identities.data ?? [], sending: sending, error: sendError }) }))] }));
149
175
  }
@@ -21,4 +21,4 @@ export type { ThreadViewProps } from './ThreadView.tsx';
21
21
  export { Composer, parseAddresses } from './Composer.tsx';
22
22
  export type { ComposerProps } from './Composer.tsx';
23
23
  export { Mail } from './Mail.tsx';
24
- export type { MailProps } from './Mail.tsx';
24
+ export type { MailLocation, MailProps } from './Mail.tsx';
@@ -1,4 +1,4 @@
1
- import type { MailClient } from './client.ts';
1
+ import { type MailClient } from './client.ts';
2
2
  import type { Draft, EmailAddress, MailIdentity, MailboxNode, Sent } from './types.ts';
3
3
  /** The JMAP `Email` object a draft becomes. Exported so the shape can be
4
4
  * asserted without a server. */
@@ -1,3 +1,4 @@
1
+ import { JAM_FETCH } from "./client.js";
1
2
  import { MailError, guard } from "./errors.js";
2
3
  import { identities } from "./identities.js";
3
4
  import { findRole, mailboxes } from "./mailboxes.js";
@@ -119,7 +120,7 @@ export async function send(client, draft, options = {}) {
119
120
  const [created] = await jam.api.Email.set({
120
121
  accountId,
121
122
  create: { draft: draftToEmail(draft, from, drafts.id) },
122
- });
123
+ }, JAM_FETCH);
123
124
  const email = created.created?.draft;
124
125
  if (!email)
125
126
  failed('draft', created.notCreated);
@@ -137,7 +138,7 @@ export async function send(client, draft, options = {}) {
137
138
  // declare the mail capability as well as submission. jmap-jam derives
138
139
  // capabilities from the method names alone and would send only
139
140
  // submission and core.
140
- { using: ['urn:ietf:params:jmap:mail'] });
141
+ { using: ['urn:ietf:params:jmap:mail'], ...JAM_FETCH });
141
142
  const submission = submitted.created?.submission;
142
143
  if (!submission)
143
144
  failed('submission', submitted.notCreated);
@@ -1,4 +1,4 @@
1
- import type { MailClient } from './client.ts';
1
+ import { type MailClient } from './client.ts';
2
2
  import type { EmailAddress, Message, ThreadDetail } from './types.ts';
3
3
  /** A body part as `Email/get` returns it. */
4
4
  type BodyPart = {
@@ -1,3 +1,4 @@
1
+ import { JAM_FETCH } from "./client.js";
1
2
  import { MailError, guard } from "./errors.js";
2
3
  import { expandTemplate } from "./uri.js";
3
4
  const MESSAGE_PROPERTIES = [
@@ -125,7 +126,7 @@ export async function thread(client, threadId, options = {}) {
125
126
  maxBodyValueBytes: options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES,
126
127
  });
127
128
  return { threads, messages };
128
- });
129
+ }, JAM_FETCH);
129
130
  const found = results.threads.list;
130
131
  if (found.length === 0) {
131
132
  throw new MailError(`no thread ${threadId} in this account`, {
@@ -1,4 +1,4 @@
1
- import type { MailClient } from './client.ts';
1
+ import { type MailClient } from './client.ts';
2
2
  import type { EmailAddress, MailFilter, MailSort, ThreadPage, ThreadSummary } from './types.ts';
3
3
  type SummaryEmail = {
4
4
  id: string;
@@ -1,3 +1,4 @@
1
+ import { JAM_FETCH } from "./client.js";
1
2
  import { guard } from "./errors.js";
2
3
  /** What a list row needs from the newest message in each thread. */
3
4
  const SUMMARY_PROPERTIES = [
@@ -121,7 +122,7 @@ export async function queryThreads(client, filter, options = {}) {
121
122
  properties: MEMBER_PROPERTIES,
122
123
  });
123
124
  return { query, latest, threads, members };
124
- });
125
+ }, JAM_FETCH);
125
126
  const query = results.query;
126
127
  const latest = results.latest;
127
128
  const threads = results.threads;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wtfalch/email",
3
- "version": "0.1.1",
3
+ "version": "0.4.0",
4
4
  "description": "The wtfalch estate: reading a mailbox over JMAP, and administering the Stalwart server it lives on. Two entries with no code in common.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -33,11 +33,14 @@
33
33
  },
34
34
  "./package.json": "./package.json"
35
35
  },
36
- "files": [
37
- "dist",
38
- "LICENSE",
39
- "README.md"
40
- ],
36
+ "files": ["dist", "LICENSE", "README.md"],
37
+ "scripts": {
38
+ "build": "rm -rf dist && tsc -p tsconfig.build.json && cp src/mailbox/react/mail.css dist/mailbox/mail.css",
39
+ "prepack": "pnpm build",
40
+ "typecheck": "tsc --noEmit",
41
+ "test": "vitest run",
42
+ "record-fixtures": "node scripts/record-fixtures.mjs"
43
+ },
41
44
  "peerDependencies": {
42
45
  "@wtfalch/design": ">=0.4.0",
43
46
  "react": "^19.0.0",
@@ -72,20 +75,8 @@
72
75
  "engines": {
73
76
  "node": ">=22.0.0"
74
77
  },
75
- "keywords": [
76
- "jmap",
77
- "email",
78
- "mail",
79
- "stalwart",
80
- "rfc8621"
81
- ],
78
+ "keywords": ["jmap", "email", "mail", "stalwart", "rfc8621"],
82
79
  "dependencies": {
83
80
  "jmap-jam": "^0.13.6"
84
- },
85
- "scripts": {
86
- "build": "rm -rf dist && tsc -p tsconfig.build.json && cp src/mailbox/react/mail.css dist/mailbox/mail.css",
87
- "typecheck": "tsc --noEmit",
88
- "test": "vitest run",
89
- "record-fixtures": "node scripts/record-fixtures.mjs"
90
81
  }
91
- }
82
+ }
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 William Tallis Falch
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.