@madauth/server 0.1.0 → 0.2.0-beta.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.
@@ -0,0 +1,19 @@
1
+ import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
2
+ import { S as StoreAdapter } from '../schema-BSgYVRpd.js';
3
+
4
+ interface DynamoDbAdapterOptions {
5
+ /** The table, with the partition key `pk` and the sort key `sk` (both strings). */
6
+ tableName: string;
7
+ /** Default: a client with the region and credentials of the environment. */
8
+ client?: Pick<DynamoDBClient, 'send'>;
9
+ }
10
+ /**
11
+ * Stores madAuth's records in one DynamoDB table. Create the table yourself, with the partition key `pk`
12
+ * and the sort key `sk`, both of type string (see "DynamoDB" in docs/server.md).
13
+ *
14
+ * A filter must contain the record's id, a unique field or an indexed field: the adapter never scans the
15
+ * table. Unique and indexed fields can't be changed by `update`.
16
+ */
17
+ declare function createDynamoDbAdapter(options: DynamoDbAdapterOptions): StoreAdapter;
18
+
19
+ export { type DynamoDbAdapterOptions, createDynamoDbAdapter };
@@ -1,4 +1,4 @@
1
- import { S as StoreAdapter } from '../schema-Dotc6LU7.js';
1
+ import { S as StoreAdapter } from '../schema-BSgYVRpd.js';
2
2
 
3
3
  /**
4
4
  * Stores madAuth's records in a SQLite file using Node's built-in `node:sqlite` (Node 22.13 or newer).
package/dist/testing.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { S as StoreAdapter } from './schema-Dotc6LU7.js';
1
+ import { S as StoreAdapter } from './schema-BSgYVRpd.js';
2
2
 
3
3
  /** The parts of a test runner the contract needs; Vitest, Jest and node:test with `expect` all fit. */
4
4
  interface ContractRunner {
package/dist/testing.js CHANGED
@@ -13,12 +13,26 @@ function testUser(overrides = {}) {
13
13
  name: null,
14
14
  sessionVersion: 0,
15
15
  lastMailAt: 0,
16
+ wrongCodes: 0,
16
17
  createdAt: 17e11,
17
18
  ...overrides
18
19
  };
19
20
  }
20
- function testVerification(userId) {
21
- return { id: unique("ver"), userId, purpose: "reset", codeHash: "h", attempts: 0, expiresAt: 17e11 };
21
+ function testAccount(overrides = {}) {
22
+ const id = unique("acc_contract");
23
+ return {
24
+ id,
25
+ userId: unique("usr_contract"),
26
+ key: `password:${id}`,
27
+ secret: "hash",
28
+ failedAttempts: 0,
29
+ lockedUntil: 0,
30
+ createdAt: 17e11,
31
+ ...overrides
32
+ };
33
+ }
34
+ function testVerification(userId, purpose = "reset") {
35
+ return { id: unique("ver"), userId, purpose, codeHash: "h", attempts: 0, expiresAt: 17e11 };
22
36
  }
23
37
  function storeAdapterContract(t, createAdapter) {
24
38
  const { describe, it, expect } = t;
@@ -92,6 +106,55 @@ function storeAdapterContract(t, createAdapter) {
92
106
  const counts = await Promise.all(Array.from({ length: 10 }, () => store.delete("verification", { id: record.id })));
93
107
  expect(counts.reduce((sum, n) => sum + n, 0)).toBe(1);
94
108
  });
109
+ it("finds a record by a unique field other than its id", async () => {
110
+ const store = await createAdapter();
111
+ const account = testAccount();
112
+ await store.create("account", account);
113
+ expect(await store.findOne("account", { key: account.key })).toEqual(account);
114
+ expect(await store.findOne("account", { key: unique("password:missing") })).toBe(null);
115
+ });
116
+ it("sets a field to a value and to null", async () => {
117
+ const store = await createAdapter();
118
+ const user = testUser({ name: "Ada" });
119
+ await store.create("user", user);
120
+ expect(await store.update("user", { id: user.id }, { name: "Grace" })).toBe(1);
121
+ expect((await store.findOne("user", { id: user.id }))?.name).toBe("Grace");
122
+ expect(await store.update("user", { id: user.id }, { name: null })).toBe(1);
123
+ expect(await store.findOne("user", { id: user.id })).toEqual({ ...user, name: null });
124
+ expect(await store.update("user", { id: user.id, name: null }, { name: "Ada" })).toBe(1);
125
+ expect(await store.update("user", { id: user.id, name: null }, { name: "Grace" })).toBe(0);
126
+ expect((await store.findOne("user", { id: user.id }))?.name).toBe("Ada");
127
+ });
128
+ it("updates a record only while it still matches every field of the filter", async () => {
129
+ const store = await createAdapter();
130
+ const account = testAccount();
131
+ await store.create("account", account);
132
+ const unchanged = { id: account.id, failedAttempts: 0 };
133
+ expect(await store.update("account", unchanged, { failedAttempts: 1, lockedUntil: 5 })).toBe(1);
134
+ expect(await store.update("account", unchanged, { failedAttempts: 1, lockedUntil: 9 })).toBe(0);
135
+ expect(await store.findOne("account", { id: account.id })).toEqual({ ...account, failedAttempts: 1, lockedUntil: 5 });
136
+ });
137
+ it("applies only one of several concurrent updates with the same filter", async () => {
138
+ const store = await createAdapter();
139
+ const account = testAccount();
140
+ await store.create("account", account);
141
+ const counts = await Promise.all(
142
+ Array.from({ length: 10 }, () => store.update("account", { id: account.id, failedAttempts: 0 }, { failedAttempts: 1 }))
143
+ );
144
+ expect(counts.reduce((sum, n) => sum + n, 0)).toBe(1);
145
+ });
146
+ it("deletes every record that matches an indexed field", async () => {
147
+ const store = await createAdapter();
148
+ const userId = unique("usr_contract");
149
+ const other = testVerification(unique("usr_contract"));
150
+ for (const purpose of ["verify", "reset", "reset"]) await store.create("verification", testVerification(userId, purpose));
151
+ await store.create("verification", other);
152
+ expect(await store.delete("verification", { userId, purpose: "verify" })).toBe(1);
153
+ expect((await store.findMany("verification", { userId })).length).toBe(2);
154
+ expect(await store.delete("verification", { userId })).toBe(2);
155
+ expect(await store.findMany("verification", { userId })).toEqual([]);
156
+ expect(await store.findOne("verification", { id: other.id })).toEqual(other);
157
+ });
95
158
  });
96
159
  }
97
160
  export {
@@ -5,6 +5,8 @@ interface MadauthUser {
5
5
  email?: string;
6
6
  name?: string;
7
7
  picture?: string;
8
+ /** The roles of the user's e-mail address, e.g. `['admin']`. Absent without roles. */
9
+ roles?: string[];
8
10
  }
9
11
 
10
12
  export type { MadauthUser as M };
package/dist/verify.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { JWTVerifyGetKey } from 'jose';
2
- import { M as MadauthUser } from './user-3DDyggFV.js';
2
+ import { M as MadauthUser } from './user-C1A3BfOo.js';
3
3
 
4
4
  interface SessionVerifierOptions {
5
5
  /** The madAuth server's public base URL (its MADAUTH_ISSUER). */
package/dist/verify.js CHANGED
@@ -2,7 +2,7 @@ import {
2
2
  SESSION_COOKIE,
3
3
  SESSION_TYP,
4
4
  userFromClaims
5
- } from "./chunk-PRRKLEW7.js";
5
+ } from "./chunk-7CTAB3QV.js";
6
6
  import {
7
7
  SIGNING_ALG
8
8
  } from "./chunk-JDQLSL36.js";
@@ -1,5 +1,5 @@
1
1
  /** Every `type` madAuth sends. */
2
- declare const WEBHOOK_TYPES: readonly ["email.verify", "email.reset", "email.already_registered", "signup.before", "user.created", "email.verified", "password.reset", "user.signed_in"];
2
+ declare const WEBHOOK_TYPES: readonly ["email.verify", "email.reset", "email.already_registered", "signup.before", "user.created", "email.verified", "password.reset", "user.signed_in", "user.deleted", "roles.changed"];
3
3
  type WebhookType = (typeof WEBHOOK_TYPES)[number];
4
4
  /** How old a call may be before receivers reject it, so a captured call can't be replayed. */
5
5
  declare const WEBHOOK_TOLERANCE_SECONDS: number;
@@ -24,8 +24,8 @@ declare function verifyWebhook(secret: string, headers: HeaderSource, body: stri
24
24
  interface WebhookSettings {
25
25
  url: string;
26
26
  secret: string;
27
- /** Types to send besides the e-mails (WEBHOOK_EVENTS); null sends every type. */
28
- events: ReadonlySet<string> | null;
27
+ /** The types that are sent (WEBHOOK_EVENTS); all others are skipped. */
28
+ events: ReadonlySet<string>;
29
29
  }
30
30
 
31
31
  export { type WebhookSettings as W, WEBHOOK_TOLERANCE_SECONDS as a, WEBHOOK_TYPES as b, type WebhookType as c, generateWebhookSecret as g, signWebhook as s, verifyWebhook as v };
package/dist/webhook.d.ts CHANGED
@@ -1 +1 @@
1
- export { a as WEBHOOK_TOLERANCE_SECONDS, b as WEBHOOK_TYPES, c as WebhookType, g as generateWebhookSecret, s as signWebhook, v as verifyWebhook } from './webhook-CT80emTQ.js';
1
+ export { a as WEBHOOK_TOLERANCE_SECONDS, b as WEBHOOK_TYPES, c as WebhookType, g as generateWebhookSecret, s as signWebhook, v as verifyWebhook } from './webhook-ltwulgcB.js';
package/dist/webhook.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  generateWebhookSecret,
5
5
  signWebhook,
6
6
  verifyWebhook
7
- } from "./chunk-AVZE6FFR.js";
7
+ } from "./chunk-6EJDVZCD.js";
8
8
  export {
9
9
  WEBHOOK_TOLERANCE_SECONDS,
10
10
  WEBHOOK_TYPES,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@madauth/server",
3
- "version": "0.1.0",
3
+ "version": "0.2.0-beta.1",
4
4
  "description": "madAuth server: Google and e-mail & password sign-in with madAuth sessions, for Docker, AWS Lambda and Azure Functions.",
5
5
  "license": "MIT",
6
6
  "author": "David Neuy",
@@ -23,7 +23,8 @@
23
23
  "jwt",
24
24
  "hono",
25
25
  "aws-lambda",
26
- "azure-functions"
26
+ "azure-functions",
27
+ "dynamodb"
27
28
  ],
28
29
  "type": "module",
29
30
  "engines": {
@@ -44,6 +45,10 @@
44
45
  "types": "./dist/store/sqlite.d.ts",
45
46
  "import": "./dist/sqlite.js"
46
47
  },
48
+ "./dynamodb": {
49
+ "types": "./dist/store/dynamodb.d.ts",
50
+ "import": "./dist/dynamodb.js"
51
+ },
47
52
  "./testing": {
48
53
  "types": "./dist/testing.d.ts",
49
54
  "import": "./dist/testing.js"
@@ -87,14 +92,19 @@
87
92
  "jose": "^6.2.12"
88
93
  },
89
94
  "peerDependencies": {
95
+ "@aws-sdk/client-dynamodb": "^3.0.0",
90
96
  "@azure/functions": "^4.0.0"
91
97
  },
92
98
  "peerDependenciesMeta": {
99
+ "@aws-sdk/client-dynamodb": {
100
+ "optional": true
101
+ },
93
102
  "@azure/functions": {
94
103
  "optional": true
95
104
  }
96
105
  },
97
106
  "devDependencies": {
107
+ "@aws-sdk/client-dynamodb": "^3.1146.0",
98
108
  "@azure/functions": "^4.16.5",
99
109
  "@types/aws-lambda": "^8.10.164",
100
110
  "@types/node": "^26.6.4",