@softr/sdk 0.0.24 → 0.0.26-alpha-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.
Files changed (62) hide show
  1. package/dist/action.d.ts +195 -0
  2. package/dist/action.js +21 -0
  3. package/dist/assertions.js +1 -7
  4. package/dist/auth/isomorphic.d.ts +1 -2
  5. package/dist/auth/methods/authenticator.d.ts +80 -0
  6. package/dist/auth/methods/authenticator.js +175 -0
  7. package/dist/auth/methods/code.d.ts +40 -0
  8. package/dist/auth/methods/code.js +74 -0
  9. package/dist/auth/methods/magicLink.d.ts +24 -0
  10. package/dist/auth/methods/magicLink.js +53 -0
  11. package/dist/auth/methods/password.d.ts +19 -5
  12. package/dist/auth/methods/password.js +36 -15
  13. package/dist/auth/methods/signup.d.ts +22 -14
  14. package/dist/auth/methods/signup.js +48 -54
  15. package/dist/auth/methods/social.d.ts +35 -0
  16. package/dist/auth/methods/social.js +58 -0
  17. package/dist/auth/methods/utils.d.ts +1 -1
  18. package/dist/auth/permissions.d.ts +7 -2
  19. package/dist/auth/platform.js +185 -7
  20. package/dist/auth/server.d.ts +15 -0
  21. package/dist/auth/server.js +16 -1
  22. package/dist/auth/session.js +35 -2
  23. package/dist/cli.d.ts +1 -0
  24. package/dist/cli.js +23 -0
  25. package/dist/codegen/datasource-types.js +471 -0
  26. package/dist/codegen/fetch-metadata.js +46 -0
  27. package/dist/codegen/softr-env.js +102 -0
  28. package/dist/codegen/user-groups.js +11 -0
  29. package/dist/core/integration.js +1 -1
  30. package/dist/datasource/chart.d.ts +196 -0
  31. package/dist/datasource/chart.js +289 -0
  32. package/dist/datasource/datasource.d.ts +16 -0
  33. package/dist/datasource/datasource.js +17 -0
  34. package/dist/datasource/fields.d.ts +122 -0
  35. package/dist/datasource/fields.js +82 -0
  36. package/dist/datasource/filter.d.ts +30 -0
  37. package/dist/datasource/filter.js +146 -0
  38. package/dist/datasource/record.d.ts +138 -0
  39. package/dist/datasource/record.js +258 -0
  40. package/dist/index.d.ts +3 -5
  41. package/dist/index.js +2 -2
  42. package/dist/package.js +5 -0
  43. package/dist/routes/visibility.d.ts +1 -1
  44. package/dist/server.d.ts +1 -1
  45. package/dist/server.js +1 -1
  46. package/package.json +31 -8
  47. package/skills/softr-sdk/SKILL.md +11 -7
  48. package/skills/softr-sdk/auth/SKILL.md +222 -36
  49. package/skills/softr-sdk/charts-and-metrics/SKILL.md +178 -0
  50. package/skills/softr-sdk/charts-and-metrics/references/aggregations.md +76 -0
  51. package/skills/softr-sdk/current-user/SKILL.md +1 -1
  52. package/skills/softr-sdk/data-mutations/SKILL.md +76 -56
  53. package/skills/softr-sdk/reading-records/SKILL.md +77 -93
  54. package/skills/softr-sdk/reading-records/references/field-types.md +41 -48
  55. package/skills/softr-sdk/reading-records/references/query-builder.md +66 -70
  56. package/skills/softr-sdk/routes-and-visibility/SKILL.md +1 -1
  57. package/dist/core/datasource.d.ts +0 -31
  58. package/dist/core/datasource.js +0 -226
  59. package/dist/core/query.d.ts +0 -354
  60. package/dist/core/query.js +0 -528
  61. package/dist/core/utils.js +0 -14
  62. package/dist/types.d.ts +0 -128
@@ -0,0 +1,195 @@
1
+ import { SoftrUserGroupId } from "./auth/user.js";
2
+
3
+ //#region src/action.d.ts
4
+ type ActionOperation = "read" | "create" | "update" | "delete";
5
+ type ActionPermission = {
6
+ userGroup: "ALL_USERS";
7
+ } | {
8
+ userGroup: "NON_LOGGED_IN_USERS";
9
+ } | {
10
+ userGroup: "LOGGED_IN_USERS";
11
+ userGroupIds?: Array<SoftrUserGroupId>;
12
+ };
13
+ /**
14
+ * Describes the datasources available in the app and their read, create and update types.
15
+ *
16
+ * This lets datasource IDs and fields in actions be type-checked.
17
+ *
18
+ * @example
19
+ * declare module "@softr/sdk" {
20
+ * interface SoftrDatasources {
21
+ * FrkQIVpeTtfJDT: {
22
+ * name: "Cafes"
23
+ * primaryField: "9QrOA"
24
+ * read: CafesRead
25
+ * create: CafesCreate
26
+ * update: CafesUpdate
27
+ * fieldTypes: { "9QrOA": "SINGLE_LINE_TEXT"; "1G3aA": "DATETIME" }
28
+ * }
29
+ * }
30
+ * }
31
+ */
32
+ interface SoftrDatasources {}
33
+ type SoftrDatasourceId = keyof SoftrDatasources extends never ? string : keyof SoftrDatasources & string;
34
+ /** The read type one datasource returns. */
35
+ type ReadOf<TDatasource$1> = TDatasource$1 extends keyof SoftrDatasources ? SoftrDatasources[TDatasource$1] extends {
36
+ read: infer TRecord;
37
+ } ? TRecord : never : never;
38
+ /**
39
+ * The DSv2 type name one field declares, as codegen emitted it: `"NUMBER"`, `"SELECT[]"`.
40
+ * `never` for a field no augmentation describes.
41
+ */
42
+ type FieldTypeOf<TDatasource$1, TFieldId> = TDatasource$1 extends keyof SoftrDatasources ? SoftrDatasources[TDatasource$1] extends {
43
+ fieldTypes: infer TTypes;
44
+ } ? TFieldId extends keyof TTypes ? TTypes[TFieldId] : never : never : never;
45
+ /** The write type one datasource accepts for one operation. */
46
+ type WriteOf<TDatasource$1, TOperation$1> = TDatasource$1 extends keyof SoftrDatasources ? TOperation$1 extends keyof SoftrDatasources[TDatasource$1] ? SoftrDatasources[TDatasource$1][TOperation$1] : never : never;
47
+ /** The field ids a write may name. Any string, until {@link SoftrDatasources} is declared. */
48
+ type FieldIdOf<TDatasource$1, TOperation$1> = keyof SoftrDatasources extends never ? string : keyof WriteOf<TDatasource$1, TOperation$1> & string;
49
+ /**
50
+ * The field ids a read may name. Any string, until {@link SoftrDatasources} is declared.
51
+ *
52
+ * A read constrains against the record type rather than a write type, because a read may
53
+ * name a field no write can: a formula or a lookup is on the record and on neither.
54
+ */
55
+ type ReadFieldIdOf<TDatasource$1> = keyof SoftrDatasources extends never ? string : keyof ReadOf<TDatasource$1> & string;
56
+ /**
57
+ * What an action declares, as the type layer reads it. `permission` is absent because no
58
+ * call site depends on it, and the build reads it off the value.
59
+ */
60
+ type ActionShape = {
61
+ id: string;
62
+ datasource: SoftrDatasourceId;
63
+ operation: ActionOperation;
64
+ fields?: Record<string, string>;
65
+ };
66
+ /** What a read declares. A read is the whole field list, so `fields` is not optional. */
67
+ type ReadShape = ActionShape & {
68
+ operation: "read";
69
+ fields: Record<string, string>;
70
+ };
71
+ /**
72
+ * What one action writes: its own aliases, each resolved to the value that field accepts.
73
+ *
74
+ * An alias renames the field, and a mapped type keeps a property's docstring only while it
75
+ * keeps the key, so the prose that sits on the field in `softr-env.d.ts` does not reach the
76
+ * call site. The type does.
77
+ */
78
+ type ActionData<TShape extends ActionShape> = keyof SoftrDatasources extends never ? Record<string, unknown> : TShape extends {
79
+ datasource: infer TDatasource;
80
+ operation: infer TOperation;
81
+ fields: infer TFields;
82
+ } ? { [Alias in keyof TFields]?: WriteOf<TDatasource, TOperation>[TFields[Alias] & keyof WriteOf<TDatasource, TOperation>] } : Record<string, unknown>;
83
+ /**
84
+ * What a mutation reads back: the same aliases, resolved against the table's read type.
85
+ *
86
+ * A write returns the record it wrote, and the SDK aliases its fields on the way out, so a
87
+ * field the action never declared is absent from the result as well as from the payload.
88
+ *
89
+ * Every alias the action does declare is present. An empty field reads as `null`, so nothing
90
+ * here is optional.
91
+ */
92
+ type ActionRecord<TShape extends ActionShape> = keyof SoftrDatasources extends never ? Record<string, unknown> : TShape extends {
93
+ datasource: infer TDatasource;
94
+ fields: infer TFields;
95
+ } ? { [Alias in keyof TFields]: ReadOf<TDatasource>[TFields[Alias] & keyof ReadOf<TDatasource>] } : Record<string, unknown>;
96
+ declare const brand: unique symbol;
97
+ /**
98
+ * A handle to a declared action, and the only thing a mutation accepts. The brand keeps it
99
+ * nominal, so an object literal of the same shape will not do and no mutation can be reached
100
+ * without passing through a declaration the build reads.
101
+ *
102
+ * The brand carries the declaration itself, which is what lets a call site resolve what the
103
+ * action writes without codegen having said anything about this action.
104
+ */
105
+ type Action<TShape extends ActionShape = ActionShape> = {
106
+ type: "action";
107
+ id: TShape["id"];
108
+ } & {
109
+ readonly [brand]: TShape;
110
+ };
111
+ /** What a write action declares. */
112
+ type WriteDeclaration<TId extends string, TDatasource$1, TOperation$1, TFields$1> = {
113
+ id: TId;
114
+ datasource: TDatasource$1;
115
+ operation: TOperation$1;
116
+ fields: TFields$1;
117
+ permission: ActionPermission;
118
+ };
119
+ /** What a read action declares. */
120
+ type ReadDeclaration<TId extends string, TDatasource$1, TFields$1> = {
121
+ id: TId;
122
+ datasource: TDatasource$1;
123
+ operation: "read";
124
+ fields: TFields$1;
125
+ permission: ActionPermission;
126
+ };
127
+ /** What a delete action declares. `fields?: never` so a stray one is rejected rather than ignored. */
128
+ type DeleteDeclaration<TId extends string, TDatasource$1> = {
129
+ id: TId;
130
+ datasource: TDatasource$1;
131
+ operation: "delete";
132
+ fields?: never;
133
+ permission: ActionPermission;
134
+ };
135
+ /** The whole declaration, which is what an action is at runtime and in the manifest. */
136
+
137
+ /**
138
+ * Declares a data operation: which datasource, which of the four operations, which fields
139
+ * participate, and who may run it.
140
+ *
141
+ * One call per action, at module scope, with a literal: the build reads these declarations
142
+ * to publish what the app may read and write.
143
+ *
144
+ * The keys of `fields` are aliases the app chooses and the values are field ids of the
145
+ * datasource the action names. The result, `data`, `where` and `orderBy` at the call site
146
+ * are all keyed by those aliases, so nothing there restates what the action already says.
147
+ *
148
+ * A read fixes the table and the columns. It does not fix which rows: `where`, `orderBy` and
149
+ * the paging arguments stay at the call site, because a list screen changes them as the user
150
+ * types and a declaration cannot follow that.
151
+ *
152
+ * @example
153
+ * const publishedPosts = defineAction({
154
+ * id: "publishedPosts",
155
+ * datasource: "8f14e45f-ceea-467a-9d0b-2c1a0e3b5d77",
156
+ * operation: "read",
157
+ * fields: { title: "V1StGXR8", author: "Z7pQm4Kd", publishedAt: "Xp2dLh6v" },
158
+ * permission: { userGroup: "ALL_USERS" },
159
+ * })
160
+ *
161
+ * const { records } = await ds
162
+ * .list(publishedPosts)
163
+ * .where((q) => q.publishedAt.lte(today))
164
+ * .orderBy((q) => q.publishedAt.desc)
165
+ *
166
+ * @example
167
+ * const closeOrderAction = defineAction({
168
+ * id: "closeOrder",
169
+ * datasource: "8f14e45f-ceea-467a-9d0b-2c1a0e3b5d77",
170
+ * operation: "update",
171
+ * fields: { status: "V1StGXR8", closedAt: "Z7pQm4Kd" },
172
+ * permission: { userGroup: "LOGGED_IN_USERS", userGroupIds: ["grp_staff"] },
173
+ * })
174
+ *
175
+ * await ds.update(closeOrderAction, { id, data: { status: "closed" } })
176
+ */
177
+ declare function defineAction<const TId extends string, const TDatasource$1 extends SoftrDatasourceId, const TFields$1 extends Record<string, ReadFieldIdOf<TDatasource$1>>>(definition: ReadDeclaration<TId, TDatasource$1, TFields$1>): Action<{
178
+ id: TId;
179
+ datasource: TDatasource$1;
180
+ operation: "read";
181
+ fields: TFields$1;
182
+ }>;
183
+ declare function defineAction<const TId extends string, const TDatasource$1 extends SoftrDatasourceId, const TOperation$1 extends "create" | "update", const TFields$1 extends Record<string, FieldIdOf<TDatasource$1, TOperation$1>>>(definition: WriteDeclaration<TId, TDatasource$1, TOperation$1, TFields$1>): Action<{
184
+ id: TId;
185
+ datasource: TDatasource$1;
186
+ operation: TOperation$1;
187
+ fields: TFields$1;
188
+ }>;
189
+ declare function defineAction<const TId extends string, const TDatasource$1 extends SoftrDatasourceId>(definition: DeleteDeclaration<TId, TDatasource$1>): Action<{
190
+ id: TId;
191
+ datasource: TDatasource$1;
192
+ operation: "delete";
193
+ }>;
194
+ //#endregion
195
+ export { Action, ActionData, ActionRecord, ActionShape, FieldTypeOf, ReadOf, ReadShape, SoftrDatasources, defineAction };
package/dist/action.js ADDED
@@ -0,0 +1,21 @@
1
+ //#region src/action.ts
2
+ function defineAction(definition) {
3
+ return {
4
+ type: "action",
5
+ ...definition
6
+ };
7
+ }
8
+ /**
9
+ * True for anything {@link defineAction} produced, whatever its operation.
10
+ *
11
+ * Matched on `type` rather than with `instanceof`, so two copies of the SDK in one
12
+ * dependency tree still recognise each other's actions.
13
+ */
14
+ function isAction(value) {
15
+ if (typeof value !== "object" || value === null) return false;
16
+ const candidate = value;
17
+ return candidate.type === "action" && typeof candidate.id === "string" && typeof candidate.datasource === "string";
18
+ }
19
+
20
+ //#endregion
21
+ export { defineAction, isAction };
@@ -18,9 +18,6 @@ function assert(condition, message) {
18
18
  function panic(message) {
19
19
  throwAtCaller(panic, new AssertionError(unwrapErrorMessage(message)));
20
20
  }
21
- function todo(message) {
22
- throwAtCaller(todo, new UnimplementedError(unwrapErrorMessage(message)));
23
- }
24
21
  /**
25
22
  * A soft assertion that throws an error in non prod environments, including
26
23
  * staging, but logs an error in production mode.
@@ -47,9 +44,6 @@ var AssertionError = class extends Error {
47
44
  var SoftAssertionError = class extends Error {
48
45
  name = "SoftAssertionError";
49
46
  };
50
- var UnimplementedError = class extends Error {
51
- name = "UnimplementedError";
52
- };
53
47
  function unwrapErrorMessage(message) {
54
48
  return typeof message === "function" ? message() : message ?? "";
55
49
  }
@@ -65,4 +59,4 @@ function throwAtCaller(f, ex) {
65
59
  }
66
60
 
67
61
  //#endregion
68
- export { assert, panic, requireDefined, softAssert, todo };
62
+ export { assert, panic, requireDefined, softAssert };
@@ -1,6 +1,5 @@
1
1
  import { User } from "./user.js";
2
- import { DatasourcePermissions } from "../types.js";
3
- import { Permissions } from "./permissions.js";
2
+ import { DatasourcePermissions, Permissions } from "./permissions.js";
4
3
  import { Session, SoftrContext } from "./context.js";
5
4
  import * as _tanstack_react_start0 from "@tanstack/react-start";
6
5
 
@@ -0,0 +1,80 @@
1
+ import { User } from "../user.js";
2
+ import { AuthError, Result } from "./utils.js";
3
+
4
+ //#region src/auth/methods/authenticator.d.ts
5
+
6
+ /**
7
+ * Starts enrolling an authenticator app, for a sign-in that asked to set one up
8
+ * or for a signed-in visitor adding one.
9
+ *
10
+ * `otpauthUri` is what an authenticator app reads. `qrPngBase64` is that same
11
+ * URI as a PNG for `<img src="data:image/png;base64,…">`, so a page can show
12
+ * the QR code without a QR library. Neither one enrols anybody.
13
+ * {@link confirmAuthenticatorSetup} does that, once it accepts a first code.
14
+ *
15
+ * Call it again to hand back a fresh QR code, for a visitor who lost the first
16
+ * one. Only the newest one enrols them.
17
+ */
18
+ declare function startAuthenticatorSetup(): Promise<Result<{
19
+ otpauthUri: string;
20
+ qrPngBase64: string;
21
+ }, AuthError>>;
22
+ /**
23
+ * Enrols the authenticator app on its first code and signs the visitor in.
24
+ *
25
+ * This is the only call that returns the backup codes, and it returns them
26
+ * once. Nothing fetches them again, so render them before leaving the page.
27
+ */
28
+ declare function confirmAuthenticatorSetup(details: {
29
+ code: string;
30
+ }): Promise<Result<{
31
+ user: User;
32
+ backupCodes: Array<string>;
33
+ }, AuthError>>;
34
+ /**
35
+ * Skips enrolment and finishes the sign-in, for a visitor whose app offers an
36
+ * authenticator without requiring one. Only the `setupOptional` step.
37
+ *
38
+ * Later sign-ins stop asking.
39
+ */
40
+ declare function skipAuthenticatorSetup(): Promise<Result<User, AuthError>>;
41
+ /**
42
+ * Finishes a sign-in that stopped at the `verify` step, with the code the
43
+ * visitor's authenticator app shows or one of their backup codes.
44
+ *
45
+ * Each backup code works once.
46
+ */
47
+ declare function verifyAuthenticator(credentials: {
48
+ code: string;
49
+ } | {
50
+ backupCode: string;
51
+ }): Promise<Result<User, AuthError>>;
52
+ /**
53
+ * Emails the signed-in visitor the code that {@link resetAuthenticator} and
54
+ * {@link removeAuthenticator} ask for.
55
+ *
56
+ * Both change how the visitor signs in, so both want a code from their inbox
57
+ * on top of being signed in.
58
+ */
59
+ declare function sendAuthenticatorResetCode(): Promise<Result<null, AuthError>>;
60
+ /**
61
+ * Unpairs the visitor's authenticator app so they can enrol a new one with
62
+ * {@link startAuthenticatorSetup}, after a lost or replaced phone.
63
+ *
64
+ * `code` is the one {@link sendAuthenticatorResetCode} emailed.
65
+ */
66
+ declare function resetAuthenticator(details: {
67
+ code: string;
68
+ }): Promise<Result<null, AuthError>>;
69
+ /**
70
+ * Turns the visitor's authenticator app off entirely.
71
+ *
72
+ * `code` is the one {@link sendAuthenticatorResetCode} emailed. An app that
73
+ * requires an authenticator will ask them to set one up again on their next
74
+ * sign-in.
75
+ */
76
+ declare function removeAuthenticator(details: {
77
+ code: string;
78
+ }): Promise<Result<null, AuthError>>;
79
+ //#endregion
80
+ export { confirmAuthenticatorSetup, removeAuthenticator, resetAuthenticator, sendAuthenticatorResetCode, skipAuthenticatorSetup, startAuthenticatorSetup, verifyAuthenticator };
@@ -0,0 +1,175 @@
1
+ import { assert, panic } from "../../assertions.js";
2
+ import { writeCookies } from "../../cookies.js";
3
+ import { sessionConfig } from "../config.js";
4
+ import { makePlatform } from "../platform.js";
5
+ import { clearPendingAuth, makeSessionDataCookie, makeSessionTokenCookie, readPendingAuthToken, readSessionToken } from "../session.js";
6
+ import { authError } from "./utils.js";
7
+
8
+ //#region src/auth/methods/authenticator.ts
9
+ /**
10
+ * Starts enrolling an authenticator app, for a sign-in that asked to set one up
11
+ * or for a signed-in visitor adding one.
12
+ *
13
+ * `otpauthUri` is what an authenticator app reads. `qrPngBase64` is that same
14
+ * URI as a PNG for `<img src="data:image/png;base64,…">`, so a page can show
15
+ * the QR code without a QR library. Neither one enrols anybody.
16
+ * {@link confirmAuthenticatorSetup} does that, once it accepts a first code.
17
+ *
18
+ * Call it again to hand back a fresh QR code, for a visitor who lost the first
19
+ * one. Only the newest one enrols them.
20
+ */
21
+ async function startAuthenticatorSetup() {
22
+ const result = await makePlatform(sessionConfig().applicationId).authenticatorSetupInit(readPendingAuthToken() ?? readSessionToken());
23
+ if (!result.ok) {
24
+ if (result.status === 401) return authError("notAuthenticated", result.message);
25
+ if (result.status === 429) return authError("tooManyAttempts", result.message);
26
+ if (result.status >= 500) panic(`Expected authenticator setup to start, but got ${result.status}: ` + result.message);
27
+ throw new Error(`${result.message}. Check SOFTR_APPLICATION_ID and that the app allows an authenticator app.`);
28
+ }
29
+ return {
30
+ success: true,
31
+ data: result.data
32
+ };
33
+ }
34
+ /**
35
+ * Enrols the authenticator app on its first code and signs the visitor in.
36
+ *
37
+ * This is the only call that returns the backup codes, and it returns them
38
+ * once. Nothing fetches them again, so render them before leaving the page.
39
+ */
40
+ async function confirmAuthenticatorSetup(details) {
41
+ const result = await makePlatform(sessionConfig().applicationId).authenticatorSetupComplete(readPendingAuthToken() ?? readSessionToken(), details.code);
42
+ if (!result.ok) {
43
+ if (result.status === 400 || result.status === 401) return authError("invalidCode", result.message);
44
+ if (result.status === 429) return authError("tooManyAttempts", result.message);
45
+ if (result.status >= 500) panic(`Expected authenticator setup to finish, but got ${result.status}: ` + result.message);
46
+ throw new Error(`${result.message}. Check SOFTR_APPLICATION_ID and that the app allows an authenticator app.`);
47
+ }
48
+ const { session, backupCodes } = result.data;
49
+ writeCookies([makeSessionTokenCookie(session.jwt), makeSessionDataCookie({
50
+ user: session.user,
51
+ permissions: session.permissions
52
+ }, session.jwt)]);
53
+ clearPendingAuth();
54
+ return {
55
+ success: true,
56
+ data: {
57
+ user: session.user,
58
+ backupCodes
59
+ }
60
+ };
61
+ }
62
+ /**
63
+ * Skips enrolment and finishes the sign-in, for a visitor whose app offers an
64
+ * authenticator without requiring one. Only the `setupOptional` step.
65
+ *
66
+ * Later sign-ins stop asking.
67
+ */
68
+ async function skipAuthenticatorSetup() {
69
+ const result = await makePlatform(sessionConfig().applicationId).authenticatorSetupDismiss(readPendingAuthToken() ?? readSessionToken());
70
+ if (!result.ok) {
71
+ if (result.status === 401) return authError("notAuthenticated", result.message);
72
+ if (result.status === 429) return authError("tooManyAttempts", result.message);
73
+ if (result.status >= 500) panic(`Expected to skip authenticator setup, but got ${result.status}: ` + result.message);
74
+ throw new Error(`${result.message}. Check SOFTR_APPLICATION_ID and that the app allows an authenticator app.`);
75
+ }
76
+ const { session } = result.data;
77
+ assert(session, "Expected a session from skipping setup, but got none");
78
+ writeCookies([makeSessionTokenCookie(session.jwt), makeSessionDataCookie({
79
+ user: session.user,
80
+ permissions: session.permissions
81
+ }, session.jwt)]);
82
+ clearPendingAuth();
83
+ return {
84
+ success: true,
85
+ data: session.user
86
+ };
87
+ }
88
+ /**
89
+ * Finishes a sign-in that stopped at the `verify` step, with the code the
90
+ * visitor's authenticator app shows or one of their backup codes.
91
+ *
92
+ * Each backup code works once.
93
+ */
94
+ async function verifyAuthenticator(credentials) {
95
+ const result = await makePlatform(sessionConfig().applicationId).authenticatorVerify(readPendingAuthToken() ?? readSessionToken(), "code" in credentials ? { totpCode: credentials.code } : { backupCode: credentials.backupCode });
96
+ if (!result.ok) {
97
+ if (result.status === 400 || result.status === 401) return authError("invalidCode", result.message);
98
+ if (result.status === 429) return authError("tooManyAttempts", result.message);
99
+ if (result.status >= 500) panic(`Expected to verify the authenticator, but got ${result.status}: ` + result.message);
100
+ throw new Error(`${result.message}. Check SOFTR_APPLICATION_ID and that the app allows an authenticator app.`);
101
+ }
102
+ const { session } = result.data;
103
+ assert(session, "Expected a session from verification, but got none");
104
+ writeCookies([makeSessionTokenCookie(session.jwt), makeSessionDataCookie({
105
+ user: session.user,
106
+ permissions: session.permissions
107
+ }, session.jwt)]);
108
+ clearPendingAuth();
109
+ return {
110
+ success: true,
111
+ data: session.user
112
+ };
113
+ }
114
+ /**
115
+ * Emails the signed-in visitor the code that {@link resetAuthenticator} and
116
+ * {@link removeAuthenticator} ask for.
117
+ *
118
+ * Both change how the visitor signs in, so both want a code from their inbox
119
+ * on top of being signed in.
120
+ */
121
+ async function sendAuthenticatorResetCode() {
122
+ const result = await makePlatform(sessionConfig().applicationId).authenticatorSendResetCode(readSessionToken());
123
+ if (!result.ok) {
124
+ if (result.status === 401) return authError("notAuthenticated", result.message);
125
+ if (result.status === 429) return authError("tooManyAttempts", result.message);
126
+ if (result.status >= 500) panic(`Expected the reset code to be emailed, but got ${result.status}: ` + result.message);
127
+ throw new Error(`${result.message}. Check SOFTR_APPLICATION_ID and that the app allows an authenticator app.`);
128
+ }
129
+ return {
130
+ success: true,
131
+ data: null
132
+ };
133
+ }
134
+ /**
135
+ * Unpairs the visitor's authenticator app so they can enrol a new one with
136
+ * {@link startAuthenticatorSetup}, after a lost or replaced phone.
137
+ *
138
+ * `code` is the one {@link sendAuthenticatorResetCode} emailed.
139
+ */
140
+ async function resetAuthenticator(details) {
141
+ const result = await makePlatform(sessionConfig().applicationId).authenticatorReset(readSessionToken(), details.code);
142
+ if (!result.ok) {
143
+ if (result.status === 400 || result.status === 401) return authError("invalidCode", result.message);
144
+ if (result.status === 429) return authError("tooManyAttempts", result.message);
145
+ if (result.status >= 500) panic(`Expected the authenticator to be reset, but got ${result.status}: ` + result.message);
146
+ throw new Error(`${result.message}. Check SOFTR_APPLICATION_ID and that the app allows an authenticator app.`);
147
+ }
148
+ return {
149
+ success: true,
150
+ data: null
151
+ };
152
+ }
153
+ /**
154
+ * Turns the visitor's authenticator app off entirely.
155
+ *
156
+ * `code` is the one {@link sendAuthenticatorResetCode} emailed. An app that
157
+ * requires an authenticator will ask them to set one up again on their next
158
+ * sign-in.
159
+ */
160
+ async function removeAuthenticator(details) {
161
+ const result = await makePlatform(sessionConfig().applicationId).authenticatorRemove(readSessionToken(), details.code);
162
+ if (!result.ok) {
163
+ if (result.status === 400 || result.status === 401) return authError("invalidCode", result.message);
164
+ if (result.status === 429) return authError("tooManyAttempts", result.message);
165
+ if (result.status >= 500) panic(`Expected the authenticator to be removed, but got ${result.status}: ` + result.message);
166
+ throw new Error(`${result.message}. Check SOFTR_APPLICATION_ID and that the app allows an authenticator app.`);
167
+ }
168
+ return {
169
+ success: true,
170
+ data: null
171
+ };
172
+ }
173
+
174
+ //#endregion
175
+ export { confirmAuthenticatorSetup, removeAuthenticator, resetAuthenticator, sendAuthenticatorResetCode, skipAuthenticatorSetup, startAuthenticatorSetup, verifyAuthenticator };
@@ -0,0 +1,40 @@
1
+ import { User } from "../user.js";
2
+ import { AuthError, Result } from "./utils.js";
3
+
4
+ //#region src/auth/methods/code.d.ts
5
+
6
+ /**
7
+ * Emails a one-time sign-in code, for an app that signs people in with a code
8
+ * rather than a password.
9
+ *
10
+ * It succeeds whether or not the address has an account, so nobody can use the
11
+ * form it backs to find out who is registered. Say "check your email" either
12
+ * way.
13
+ */
14
+ declare function sendSignInCode(details: {
15
+ email: string;
16
+ }): Promise<Result<null, AuthError>>;
17
+ /**
18
+ * Signs a visitor in with the code {@link sendSignInCode} emailed them.
19
+ *
20
+ * A `signedIn` result means the visitor is signed in. An app that also asks for
21
+ * an authenticator app answers `authenticatorRequired` instead, naming the step
22
+ * to send them to next.
23
+ */
24
+ declare function signInWithCode(credentials: {
25
+ email: string;
26
+ code: string;
27
+ }): Promise<Result<{
28
+ status: "signedIn";
29
+ user: User;
30
+ } | {
31
+ status: "authenticatorRequired";
32
+ /**
33
+ * `verify`: they have an authenticator app and owe a code.
34
+ * `setup`: the app requires one and they have none yet.
35
+ * `setupOptional`: same, except they may skip it instead.
36
+ */
37
+ step: "verify" | "setup" | "setupOptional";
38
+ }, AuthError>>;
39
+ //#endregion
40
+ export { sendSignInCode, signInWithCode };
@@ -0,0 +1,74 @@
1
+ import { assert, panic } from "../../assertions.js";
2
+ import { writeCookies } from "../../cookies.js";
3
+ import { sessionConfig } from "../config.js";
4
+ import { makePlatform } from "../platform.js";
5
+ import { clearPendingAuth, makePendingAuthCookie, makeSessionDataCookie, makeSessionTokenCookie } from "../session.js";
6
+ import { authError } from "./utils.js";
7
+
8
+ //#region src/auth/methods/code.ts
9
+ /**
10
+ * Emails a one-time sign-in code, for an app that signs people in with a code
11
+ * rather than a password.
12
+ *
13
+ * It succeeds whether or not the address has an account, so nobody can use the
14
+ * form it backs to find out who is registered. Say "check your email" either
15
+ * way.
16
+ */
17
+ async function sendSignInCode(details) {
18
+ const result = await makePlatform(sessionConfig().applicationId).sendSignInCode(details.email);
19
+ if (!result.ok) {
20
+ if (result.status === 429) return authError("tooManyAttempts", result.message);
21
+ if (result.status === 400) return authError("invalidDetails", result.message);
22
+ if (result.status >= 500) panic(`Expected the sign-in code to be emailed, but got ${result.status}: ` + result.message);
23
+ throw new Error(`${result.message}. Check SOFTR_APPLICATION_ID and that the app signs people in with an emailed code.`);
24
+ }
25
+ return {
26
+ success: true,
27
+ data: null
28
+ };
29
+ }
30
+ /**
31
+ * Signs a visitor in with the code {@link sendSignInCode} emailed them.
32
+ *
33
+ * A `signedIn` result means the visitor is signed in. An app that also asks for
34
+ * an authenticator app answers `authenticatorRequired` instead, naming the step
35
+ * to send them to next.
36
+ */
37
+ async function signInWithCode(credentials) {
38
+ const result = await makePlatform(sessionConfig().applicationId).signInWithCode(credentials);
39
+ if (!result.ok) {
40
+ if (result.status === 429) return authError("tooManyAttempts", result.message);
41
+ if (result.status === 400 || result.status === 401) return authError("invalidCode", result.message);
42
+ if (result.status >= 500) panic(`Expected sign-in to succeed, but got ${result.status}: ` + result.message);
43
+ throw new Error(`${result.message}. Check SOFTR_APPLICATION_ID and that the app signs people in with an emailed code.`);
44
+ }
45
+ if (result.data.status === "MFA_REQUIRED") {
46
+ const { mfa } = result.data;
47
+ assert(mfa, "Expected a challenge from sign-in, but got none");
48
+ writeCookies([makePendingAuthCookie(mfa.challengeToken)]);
49
+ return {
50
+ success: true,
51
+ data: {
52
+ status: "authenticatorRequired",
53
+ step: mfa.action === "VERIFICATION_REQUIRED" ? "verify" : mfa.action === "SETUP_REQUIRED" ? "setup" : "setupOptional"
54
+ }
55
+ };
56
+ }
57
+ const { session } = result.data;
58
+ assert(session, "Expected a session from sign-in, but got none");
59
+ writeCookies([makeSessionTokenCookie(session.jwt), makeSessionDataCookie({
60
+ user: session.user,
61
+ permissions: session.permissions
62
+ }, session.jwt)]);
63
+ clearPendingAuth();
64
+ return {
65
+ success: true,
66
+ data: {
67
+ status: "signedIn",
68
+ user: session.user
69
+ }
70
+ };
71
+ }
72
+
73
+ //#endregion
74
+ export { sendSignInCode, signInWithCode };
@@ -0,0 +1,24 @@
1
+ import { User } from "../user.js";
2
+ import { AuthError, Result } from "./utils.js";
3
+
4
+ //#region src/auth/methods/magicLink.d.ts
5
+
6
+ /**
7
+ * Signs a visitor in with the token from a magic link they followed.
8
+ *
9
+ * The token keeps working after it is used, so anyone holding the link can sign
10
+ * in as that visitor. Never log it, and redirect away from the URL that carried
11
+ * it.
12
+ *
13
+ */
14
+ declare function signInWithMagicLink(credentials: {
15
+ token: string;
16
+ }): Promise<Result<{
17
+ status: "signedIn";
18
+ user: User;
19
+ } | {
20
+ status: "authenticatorRequired";
21
+ step: "verify" | "setup" | "setupOptional";
22
+ }, AuthError>>;
23
+ //#endregion
24
+ export { signInWithMagicLink };