@aooth/user 0.1.8 → 0.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/atscript-db.cjs +17 -5
- package/dist/atscript-db.d.cts +15 -3
- package/dist/atscript-db.d.mts +15 -3
- package/dist/atscript-db.mjs +17 -5
- package/dist/{federated-identity-store-oRjhnR5l.cjs → federated-identity-store-BEEEcoaP.cjs} +3 -2
- package/dist/{federated-identity-store-Cmc7jBrw.mjs → federated-identity-store-CHW1xtMp.mjs} +3 -2
- package/dist/{federated-identity-store-DEEed8lA.d.cts → federated-identity-store-CI7Vgllp.d.cts} +13 -18
- package/dist/{federated-identity-store-DEEed8lA.d.mts → federated-identity-store-CI7Vgllp.d.mts} +13 -18
- package/dist/index.cjs +49 -10
- package/dist/index.d.cts +23 -2
- package/dist/index.d.mts +23 -2
- package/dist/index.mjs +49 -10
- package/package.json +7 -7
- package/src/atscript-db/user-credentials.as +0 -3
- package/src/atscript-db/user-credentials.as.d.ts +0 -1
package/dist/atscript-db.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_federated_identity_store = require("./federated-identity-store-
|
|
2
|
+
const require_federated_identity_store = require("./federated-identity-store-BEEEcoaP.cjs");
|
|
3
3
|
let node_crypto = require("node:crypto");
|
|
4
4
|
//#region src/atscript-db/federated-identity-store.ts
|
|
5
5
|
function isConflict$1(err) {
|
|
@@ -80,14 +80,17 @@ function isConflict(err) {
|
|
|
80
80
|
* `@atscript/db`-backed `UserStore`. Pass the resolved table for the
|
|
81
81
|
* `AoothUserCredentials` (or a `.as` interface extending it) shipped at the
|
|
82
82
|
* `@aooth/user/atscript-db/model.as` subpath. Identity reads use the
|
|
83
|
-
* `@meta.id` PK (`id`)
|
|
84
|
-
* on `id`.
|
|
83
|
+
* `@meta.id` PK (`id`) and the unique `username` index, plus any
|
|
84
|
+
* annotation-resolved `handleFields`; writes key on `id`.
|
|
85
85
|
*/
|
|
86
86
|
var UsersStoreAtscriptDb = class extends require_federated_identity_store.UserStore {
|
|
87
87
|
table;
|
|
88
|
+
/** Secondary handle fields tried (in order) by `findByHandle` after `username`. */
|
|
89
|
+
handleFields;
|
|
88
90
|
constructor(opts) {
|
|
89
91
|
super();
|
|
90
92
|
this.table = opts.table;
|
|
93
|
+
this.handleFields = opts.handleFields ?? [];
|
|
91
94
|
}
|
|
92
95
|
async exists(handle) {
|
|
93
96
|
return await this.table.count({ filter: { username: handle } }) > 0;
|
|
@@ -98,7 +101,11 @@ var UsersStoreAtscriptDb = class extends require_federated_identity_store.UserSt
|
|
|
98
101
|
async findByHandle(handle) {
|
|
99
102
|
const byUsername = await this.table.findOne({ filter: { username: handle } });
|
|
100
103
|
if (byUsername) return byUsername;
|
|
101
|
-
|
|
104
|
+
for (const field of this.handleFields) {
|
|
105
|
+
const byHandle = await this.table.findOne({ filter: { [field]: handle } });
|
|
106
|
+
if (byHandle) return byHandle;
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
102
109
|
}
|
|
103
110
|
async findByIdentifier(value) {
|
|
104
111
|
const byId = await this.table.findOne({ filter: { id: value } });
|
|
@@ -119,7 +126,12 @@ var UsersStoreAtscriptDb = class extends require_federated_identity_store.UserSt
|
|
|
119
126
|
if (update.inc) for (const [path, amount] of Object.entries(update.inc)) require_federated_identity_store.setAtPath(patch, path, { $inc: amount });
|
|
120
127
|
if (Object.keys(patch).length <= 1) return true;
|
|
121
128
|
if (update.expectedVersion !== void 0) patch.$cas = { version: update.expectedVersion };
|
|
122
|
-
|
|
129
|
+
try {
|
|
130
|
+
return (await this.table.updateOne(patch)).matchedCount > 0;
|
|
131
|
+
} catch (e) {
|
|
132
|
+
if (isConflict(e)) throw new require_federated_identity_store.UserAuthError("ALREADY_EXISTS", "Update conflicts with an existing unique value");
|
|
133
|
+
throw e;
|
|
134
|
+
}
|
|
123
135
|
}
|
|
124
136
|
async delete(id) {
|
|
125
137
|
return (await this.table.deleteMany({ id })).deletedCount > 0;
|
package/dist/atscript-db.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { D as UserCredentials, i as NewFederatedIdentity, k as UserStoreUpdate, n as FederatedIdentityStore, o as UserStore, r as FederatedProfileSnapshot, s as WithCasOptions, t as FederatedIdentity } from "./federated-identity-store-
|
|
1
|
+
import { D as UserCredentials, i as NewFederatedIdentity, k as UserStoreUpdate, n as FederatedIdentityStore, o as UserStore, r as FederatedProfileSnapshot, s as WithCasOptions, t as FederatedIdentity } from "./federated-identity-store-CI7Vgllp.cjs";
|
|
2
2
|
|
|
3
3
|
//#region src/atscript-db/federated-identity-store.d.ts
|
|
4
4
|
/**
|
|
@@ -88,16 +88,28 @@ interface AuthUserTable<TUserCustom extends object = object> {
|
|
|
88
88
|
}
|
|
89
89
|
interface UsersStoreAtscriptDbOptions<TUserCustom extends object> {
|
|
90
90
|
table: AuthUserTable<TUserCustom>;
|
|
91
|
+
/**
|
|
92
|
+
* Ordered login-handle field names (e.g. email then phone), resolved from the
|
|
93
|
+
* model's `@aooth.user.*` annotations by the wiring layer (`handleFields` of
|
|
94
|
+
* `getAoothUserHandleSpec`). `findByHandle` falls back to a `{ [field]: handle }`
|
|
95
|
+
* lookup for each, in order, after `username`. Omit/empty to disable handle
|
|
96
|
+
* resolution (login by anything but `username` unavailable). The base credential
|
|
97
|
+
* model no longer hardcodes `email` — the field names are whatever the
|
|
98
|
+
* consumer's `.as` model annotates.
|
|
99
|
+
*/
|
|
100
|
+
handleFields?: string[];
|
|
91
101
|
}
|
|
92
102
|
/**
|
|
93
103
|
* `@atscript/db`-backed `UserStore`. Pass the resolved table for the
|
|
94
104
|
* `AoothUserCredentials` (or a `.as` interface extending it) shipped at the
|
|
95
105
|
* `@aooth/user/atscript-db/model.as` subpath. Identity reads use the
|
|
96
|
-
* `@meta.id` PK (`id`)
|
|
97
|
-
* on `id`.
|
|
106
|
+
* `@meta.id` PK (`id`) and the unique `username` index, plus any
|
|
107
|
+
* annotation-resolved `handleFields`; writes key on `id`.
|
|
98
108
|
*/
|
|
99
109
|
declare class UsersStoreAtscriptDb<TUserCustom extends object = object> extends UserStore<TUserCustom> {
|
|
100
110
|
protected table: AuthUserTable<TUserCustom>;
|
|
111
|
+
/** Secondary handle fields tried (in order) by `findByHandle` after `username`. */
|
|
112
|
+
protected readonly handleFields: string[];
|
|
101
113
|
constructor(opts: UsersStoreAtscriptDbOptions<TUserCustom>);
|
|
102
114
|
exists(handle: string): Promise<boolean>;
|
|
103
115
|
findById(id: string): Promise<(UserCredentials & TUserCustom) | null>;
|
package/dist/atscript-db.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { D as UserCredentials, i as NewFederatedIdentity, k as UserStoreUpdate, n as FederatedIdentityStore, o as UserStore, r as FederatedProfileSnapshot, s as WithCasOptions, t as FederatedIdentity } from "./federated-identity-store-
|
|
1
|
+
import { D as UserCredentials, i as NewFederatedIdentity, k as UserStoreUpdate, n as FederatedIdentityStore, o as UserStore, r as FederatedProfileSnapshot, s as WithCasOptions, t as FederatedIdentity } from "./federated-identity-store-CI7Vgllp.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/atscript-db/federated-identity-store.d.ts
|
|
4
4
|
/**
|
|
@@ -88,16 +88,28 @@ interface AuthUserTable<TUserCustom extends object = object> {
|
|
|
88
88
|
}
|
|
89
89
|
interface UsersStoreAtscriptDbOptions<TUserCustom extends object> {
|
|
90
90
|
table: AuthUserTable<TUserCustom>;
|
|
91
|
+
/**
|
|
92
|
+
* Ordered login-handle field names (e.g. email then phone), resolved from the
|
|
93
|
+
* model's `@aooth.user.*` annotations by the wiring layer (`handleFields` of
|
|
94
|
+
* `getAoothUserHandleSpec`). `findByHandle` falls back to a `{ [field]: handle }`
|
|
95
|
+
* lookup for each, in order, after `username`. Omit/empty to disable handle
|
|
96
|
+
* resolution (login by anything but `username` unavailable). The base credential
|
|
97
|
+
* model no longer hardcodes `email` — the field names are whatever the
|
|
98
|
+
* consumer's `.as` model annotates.
|
|
99
|
+
*/
|
|
100
|
+
handleFields?: string[];
|
|
91
101
|
}
|
|
92
102
|
/**
|
|
93
103
|
* `@atscript/db`-backed `UserStore`. Pass the resolved table for the
|
|
94
104
|
* `AoothUserCredentials` (or a `.as` interface extending it) shipped at the
|
|
95
105
|
* `@aooth/user/atscript-db/model.as` subpath. Identity reads use the
|
|
96
|
-
* `@meta.id` PK (`id`)
|
|
97
|
-
* on `id`.
|
|
106
|
+
* `@meta.id` PK (`id`) and the unique `username` index, plus any
|
|
107
|
+
* annotation-resolved `handleFields`; writes key on `id`.
|
|
98
108
|
*/
|
|
99
109
|
declare class UsersStoreAtscriptDb<TUserCustom extends object = object> extends UserStore<TUserCustom> {
|
|
100
110
|
protected table: AuthUserTable<TUserCustom>;
|
|
111
|
+
/** Secondary handle fields tried (in order) by `findByHandle` after `username`. */
|
|
112
|
+
protected readonly handleFields: string[];
|
|
101
113
|
constructor(opts: UsersStoreAtscriptDbOptions<TUserCustom>);
|
|
102
114
|
exists(handle: string): Promise<boolean>;
|
|
103
115
|
findById(id: string): Promise<(UserCredentials & TUserCustom) | null>;
|
package/dist/atscript-db.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { d as UserAuthError, n as pickDefinedProfile, r as UserStore, t as FederatedIdentityStore, u as setAtPath } from "./federated-identity-store-
|
|
1
|
+
import { d as UserAuthError, n as pickDefinedProfile, r as UserStore, t as FederatedIdentityStore, u as setAtPath } from "./federated-identity-store-CHW1xtMp.mjs";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
//#region src/atscript-db/federated-identity-store.ts
|
|
4
4
|
function isConflict$1(err) {
|
|
@@ -79,14 +79,17 @@ function isConflict(err) {
|
|
|
79
79
|
* `@atscript/db`-backed `UserStore`. Pass the resolved table for the
|
|
80
80
|
* `AoothUserCredentials` (or a `.as` interface extending it) shipped at the
|
|
81
81
|
* `@aooth/user/atscript-db/model.as` subpath. Identity reads use the
|
|
82
|
-
* `@meta.id` PK (`id`)
|
|
83
|
-
* on `id`.
|
|
82
|
+
* `@meta.id` PK (`id`) and the unique `username` index, plus any
|
|
83
|
+
* annotation-resolved `handleFields`; writes key on `id`.
|
|
84
84
|
*/
|
|
85
85
|
var UsersStoreAtscriptDb = class extends UserStore {
|
|
86
86
|
table;
|
|
87
|
+
/** Secondary handle fields tried (in order) by `findByHandle` after `username`. */
|
|
88
|
+
handleFields;
|
|
87
89
|
constructor(opts) {
|
|
88
90
|
super();
|
|
89
91
|
this.table = opts.table;
|
|
92
|
+
this.handleFields = opts.handleFields ?? [];
|
|
90
93
|
}
|
|
91
94
|
async exists(handle) {
|
|
92
95
|
return await this.table.count({ filter: { username: handle } }) > 0;
|
|
@@ -97,7 +100,11 @@ var UsersStoreAtscriptDb = class extends UserStore {
|
|
|
97
100
|
async findByHandle(handle) {
|
|
98
101
|
const byUsername = await this.table.findOne({ filter: { username: handle } });
|
|
99
102
|
if (byUsername) return byUsername;
|
|
100
|
-
|
|
103
|
+
for (const field of this.handleFields) {
|
|
104
|
+
const byHandle = await this.table.findOne({ filter: { [field]: handle } });
|
|
105
|
+
if (byHandle) return byHandle;
|
|
106
|
+
}
|
|
107
|
+
return null;
|
|
101
108
|
}
|
|
102
109
|
async findByIdentifier(value) {
|
|
103
110
|
const byId = await this.table.findOne({ filter: { id: value } });
|
|
@@ -118,7 +125,12 @@ var UsersStoreAtscriptDb = class extends UserStore {
|
|
|
118
125
|
if (update.inc) for (const [path, amount] of Object.entries(update.inc)) setAtPath(patch, path, { $inc: amount });
|
|
119
126
|
if (Object.keys(patch).length <= 1) return true;
|
|
120
127
|
if (update.expectedVersion !== void 0) patch.$cas = { version: update.expectedVersion };
|
|
121
|
-
|
|
128
|
+
try {
|
|
129
|
+
return (await this.table.updateOne(patch)).matchedCount > 0;
|
|
130
|
+
} catch (e) {
|
|
131
|
+
if (isConflict(e)) throw new UserAuthError("ALREADY_EXISTS", "Update conflicts with an existing unique value");
|
|
132
|
+
throw e;
|
|
133
|
+
}
|
|
122
134
|
}
|
|
123
135
|
async delete(id) {
|
|
124
136
|
return (await this.table.deleteMany({ id })).deletedCount > 0;
|
package/dist/{federated-identity-store-oRjhnR5l.cjs → federated-identity-store-BEEEcoaP.cjs}
RENAMED
|
@@ -100,9 +100,10 @@ function incrementAtPath(obj, path, amount) {
|
|
|
100
100
|
*
|
|
101
101
|
* - `findById` — strict, by the surrogate id; the canonical identity read used
|
|
102
102
|
* by authenticated flows that resolve the session subject (`getUserId()`).
|
|
103
|
-
* - `findByHandle` — deterministic LOGIN resolver (`username
|
|
103
|
+
* - `findByHandle` — deterministic LOGIN resolver (`username`, then the
|
|
104
|
+
* annotation-resolved handle fields — email, then phone — in order).
|
|
104
105
|
* - `findByIdentifier` — permissive internal/admin/recovery lookup (`id`, then
|
|
105
|
-
*
|
|
106
|
+
* the `findByHandle` chain).
|
|
106
107
|
*
|
|
107
108
|
* Writes (`update`/`delete`/`withCas`) all key on the surrogate `id`.
|
|
108
109
|
*/
|
package/dist/{federated-identity-store-Cmc7jBrw.mjs → federated-identity-store-CHW1xtMp.mjs}
RENAMED
|
@@ -100,9 +100,10 @@ function incrementAtPath(obj, path, amount) {
|
|
|
100
100
|
*
|
|
101
101
|
* - `findById` — strict, by the surrogate id; the canonical identity read used
|
|
102
102
|
* by authenticated flows that resolve the session subject (`getUserId()`).
|
|
103
|
-
* - `findByHandle` — deterministic LOGIN resolver (`username
|
|
103
|
+
* - `findByHandle` — deterministic LOGIN resolver (`username`, then the
|
|
104
|
+
* annotation-resolved handle fields — email, then phone — in order).
|
|
104
105
|
* - `findByIdentifier` — permissive internal/admin/recovery lookup (`id`, then
|
|
105
|
-
*
|
|
106
|
+
* the `findByHandle` chain).
|
|
106
107
|
*
|
|
107
108
|
* Writes (`update`/`delete`/`withCas`) all key on the surrogate `id`.
|
|
108
109
|
*/
|
package/dist/{federated-identity-store-DEEed8lA.d.cts → federated-identity-store-CI7Vgllp.d.cts}
RENAMED
|
@@ -2,14 +2,6 @@
|
|
|
2
2
|
interface UserCredentials {
|
|
3
3
|
id: string;
|
|
4
4
|
username: string;
|
|
5
|
-
/**
|
|
6
|
-
* Unique login/contact handle. Indexed `@db.index.unique 'email_idx'` on the
|
|
7
|
-
* `AoothUserCredentials` model so it is independently unique from `username`
|
|
8
|
-
* and resolvable via `UserStore.findByHandle` / `findByIdentifier`. Optional
|
|
9
|
-
* because not every deployment populates it (and the invite flow sets
|
|
10
|
-
* `username := email`).
|
|
11
|
-
*/
|
|
12
|
-
email?: string;
|
|
13
5
|
/**
|
|
14
6
|
* Server-managed optimistic-concurrency counter. Bumped by `UserStore.update`
|
|
15
7
|
* on every successful write; checked against `UserStoreUpdate.expectedVersion`
|
|
@@ -233,9 +225,10 @@ interface WithCasOptions {
|
|
|
233
225
|
*
|
|
234
226
|
* - `findById` — strict, by the surrogate id; the canonical identity read used
|
|
235
227
|
* by authenticated flows that resolve the session subject (`getUserId()`).
|
|
236
|
-
* - `findByHandle` — deterministic LOGIN resolver (`username
|
|
228
|
+
* - `findByHandle` — deterministic LOGIN resolver (`username`, then the
|
|
229
|
+
* annotation-resolved handle fields — email, then phone — in order).
|
|
237
230
|
* - `findByIdentifier` — permissive internal/admin/recovery lookup (`id`, then
|
|
238
|
-
*
|
|
231
|
+
* the `findByHandle` chain).
|
|
239
232
|
*
|
|
240
233
|
* Writes (`update`/`delete`/`withCas`) all key on the surrogate `id`.
|
|
241
234
|
*/
|
|
@@ -248,17 +241,19 @@ declare abstract class UserStore<T extends object = object> {
|
|
|
248
241
|
*/
|
|
249
242
|
abstract findById(id: string): Promise<(UserCredentials & T) | null>;
|
|
250
243
|
/**
|
|
251
|
-
* Deterministic LOGIN resolver: matches `username` exactly, then
|
|
252
|
-
*
|
|
253
|
-
*
|
|
254
|
-
* silently resolve a value that is one
|
|
255
|
-
* to an arbitrary account.
|
|
244
|
+
* Deterministic LOGIN resolver: matches `username` exactly, then each
|
|
245
|
+
* annotation-resolved handle field (email, then phone) exactly, in that
|
|
246
|
+
* order. Intentionally NOT a permissive `$or` — handle values are all
|
|
247
|
+
* strings, so a permissive match could silently resolve a value that is one
|
|
248
|
+
* user's username and another's email to an arbitrary account. Each handle
|
|
249
|
+
* field is unique-when-present (the `@aooth.user.*` boot contract), so a
|
|
250
|
+
* present value resolves to at most one row.
|
|
256
251
|
*/
|
|
257
252
|
abstract findByHandle(handle: string): Promise<(UserCredentials & T) | null>;
|
|
258
253
|
/**
|
|
259
|
-
* Permissive lookup for internal / admin / recovery callers: `id`, then
|
|
260
|
-
* `username`, then
|
|
261
|
-
* use `findByHandle` there.
|
|
254
|
+
* Permissive lookup for internal / admin / recovery callers: `id`, then the
|
|
255
|
+
* `findByHandle` chain (`username`, then the resolved handle fields). NOT for
|
|
256
|
+
* the login path — use `findByHandle` there.
|
|
262
257
|
*/
|
|
263
258
|
abstract findByIdentifier(value: string): Promise<(UserCredentials & T) | null>;
|
|
264
259
|
abstract create(data: UserCredentials & T): Promise<void>;
|
package/dist/{federated-identity-store-DEEed8lA.d.mts → federated-identity-store-CI7Vgllp.d.mts}
RENAMED
|
@@ -2,14 +2,6 @@
|
|
|
2
2
|
interface UserCredentials {
|
|
3
3
|
id: string;
|
|
4
4
|
username: string;
|
|
5
|
-
/**
|
|
6
|
-
* Unique login/contact handle. Indexed `@db.index.unique 'email_idx'` on the
|
|
7
|
-
* `AoothUserCredentials` model so it is independently unique from `username`
|
|
8
|
-
* and resolvable via `UserStore.findByHandle` / `findByIdentifier`. Optional
|
|
9
|
-
* because not every deployment populates it (and the invite flow sets
|
|
10
|
-
* `username := email`).
|
|
11
|
-
*/
|
|
12
|
-
email?: string;
|
|
13
5
|
/**
|
|
14
6
|
* Server-managed optimistic-concurrency counter. Bumped by `UserStore.update`
|
|
15
7
|
* on every successful write; checked against `UserStoreUpdate.expectedVersion`
|
|
@@ -233,9 +225,10 @@ interface WithCasOptions {
|
|
|
233
225
|
*
|
|
234
226
|
* - `findById` — strict, by the surrogate id; the canonical identity read used
|
|
235
227
|
* by authenticated flows that resolve the session subject (`getUserId()`).
|
|
236
|
-
* - `findByHandle` — deterministic LOGIN resolver (`username
|
|
228
|
+
* - `findByHandle` — deterministic LOGIN resolver (`username`, then the
|
|
229
|
+
* annotation-resolved handle fields — email, then phone — in order).
|
|
237
230
|
* - `findByIdentifier` — permissive internal/admin/recovery lookup (`id`, then
|
|
238
|
-
*
|
|
231
|
+
* the `findByHandle` chain).
|
|
239
232
|
*
|
|
240
233
|
* Writes (`update`/`delete`/`withCas`) all key on the surrogate `id`.
|
|
241
234
|
*/
|
|
@@ -248,17 +241,19 @@ declare abstract class UserStore<T extends object = object> {
|
|
|
248
241
|
*/
|
|
249
242
|
abstract findById(id: string): Promise<(UserCredentials & T) | null>;
|
|
250
243
|
/**
|
|
251
|
-
* Deterministic LOGIN resolver: matches `username` exactly, then
|
|
252
|
-
*
|
|
253
|
-
*
|
|
254
|
-
* silently resolve a value that is one
|
|
255
|
-
* to an arbitrary account.
|
|
244
|
+
* Deterministic LOGIN resolver: matches `username` exactly, then each
|
|
245
|
+
* annotation-resolved handle field (email, then phone) exactly, in that
|
|
246
|
+
* order. Intentionally NOT a permissive `$or` — handle values are all
|
|
247
|
+
* strings, so a permissive match could silently resolve a value that is one
|
|
248
|
+
* user's username and another's email to an arbitrary account. Each handle
|
|
249
|
+
* field is unique-when-present (the `@aooth.user.*` boot contract), so a
|
|
250
|
+
* present value resolves to at most one row.
|
|
256
251
|
*/
|
|
257
252
|
abstract findByHandle(handle: string): Promise<(UserCredentials & T) | null>;
|
|
258
253
|
/**
|
|
259
|
-
* Permissive lookup for internal / admin / recovery callers: `id`, then
|
|
260
|
-
* `username`, then
|
|
261
|
-
* use `findByHandle` there.
|
|
254
|
+
* Permissive lookup for internal / admin / recovery callers: `id`, then the
|
|
255
|
+
* `findByHandle` chain (`username`, then the resolved handle fields). NOT for
|
|
256
|
+
* the login path — use `findByHandle` there.
|
|
262
257
|
*/
|
|
263
258
|
abstract findByIdentifier(value: string): Promise<(UserCredentials & T) | null>;
|
|
264
259
|
abstract create(data: UserCredentials & T): Promise<void>;
|
package/dist/index.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_federated_identity_store = require("./federated-identity-store-
|
|
2
|
+
const require_federated_identity_store = require("./federated-identity-store-BEEEcoaP.cjs");
|
|
3
3
|
let node_crypto = require("node:crypto");
|
|
4
4
|
//#region src/base-x/base32.ts
|
|
5
5
|
/**
|
|
@@ -780,12 +780,24 @@ var UserStoreMemory = class extends require_federated_identity_store.UserStore {
|
|
|
780
780
|
/** Keyed by the stable surrogate `id` (the token subject). */
|
|
781
781
|
store = /* @__PURE__ */ new Map();
|
|
782
782
|
/**
|
|
783
|
+
* Ordered secondary handle fields (e.g. email then phone) resolved from the
|
|
784
|
+
* model's `@aooth.user.*` annotations by the wiring layer (`handleFields` of
|
|
785
|
+
* `getAoothUserHandleSpec`). Tried by `findByHandle` after `username`, and
|
|
786
|
+
* enforced unique by `create`. Empty when no handles are configured (login by
|
|
787
|
+
* email/phone unavailable).
|
|
788
|
+
*/
|
|
789
|
+
handleFields;
|
|
790
|
+
/**
|
|
783
791
|
* Optional seed. The map is keyed by each record's `id`; a record missing an
|
|
784
792
|
* `id` gets one minted (mirrors the DB `@db.default.uuid`). The seed object's
|
|
785
793
|
* keys are ignored — identity is the record's `id`.
|
|
794
|
+
*
|
|
795
|
+
* `opts.handleFields` names the ordered secondary login handles (mirroring
|
|
796
|
+
* `UsersStoreAtscriptDb`); omit it for username-only resolution.
|
|
786
797
|
*/
|
|
787
|
-
constructor(seed) {
|
|
798
|
+
constructor(seed, opts) {
|
|
788
799
|
super();
|
|
800
|
+
this.handleFields = opts?.handleFields ?? [];
|
|
789
801
|
if (seed) for (const value of Object.values(seed)) {
|
|
790
802
|
const cloned = structuredClone(value);
|
|
791
803
|
if (!cloned.id) cloned.id = (0, node_crypto.randomUUID)();
|
|
@@ -801,12 +813,21 @@ var UserStoreMemory = class extends require_federated_identity_store.UserStore {
|
|
|
801
813
|
return user ? structuredClone(user) : null;
|
|
802
814
|
}
|
|
803
815
|
async findByHandle(handle) {
|
|
804
|
-
let
|
|
816
|
+
let byHandle = null;
|
|
805
817
|
for (const u of this.store.values()) {
|
|
806
818
|
if (u.username === handle) return structuredClone(u);
|
|
807
|
-
if (
|
|
819
|
+
if (byHandle === null) {
|
|
820
|
+
const rec = u;
|
|
821
|
+
for (const field of this.handleFields) {
|
|
822
|
+
const value = rec[field];
|
|
823
|
+
if (value !== void 0 && value === handle) {
|
|
824
|
+
byHandle = u;
|
|
825
|
+
break;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
}
|
|
808
829
|
}
|
|
809
|
-
return
|
|
830
|
+
return byHandle ? structuredClone(byHandle) : null;
|
|
810
831
|
}
|
|
811
832
|
async findByIdentifier(value) {
|
|
812
833
|
const byId = this.store.get(value);
|
|
@@ -816,18 +837,36 @@ var UserStoreMemory = class extends require_federated_identity_store.UserStore {
|
|
|
816
837
|
async create(data) {
|
|
817
838
|
const cloned = structuredClone(data);
|
|
818
839
|
if (!cloned.id) cloned.id = (0, node_crypto.randomUUID)();
|
|
819
|
-
for (const u of this.store.values()) {
|
|
820
|
-
|
|
821
|
-
if (cloned.email !== void 0 && u.email === cloned.email) throw new require_federated_identity_store.UserAuthError("ALREADY_EXISTS", `Email "${cloned.email}" already exists`);
|
|
822
|
-
}
|
|
840
|
+
for (const u of this.store.values()) if (u.username === cloned.username) throw new require_federated_identity_store.UserAuthError("ALREADY_EXISTS", `User "${cloned.username}" already exists`);
|
|
841
|
+
this.assertHandlesUnique(cloned);
|
|
823
842
|
cloned.version = 0;
|
|
824
843
|
this.store.set(cloned.id, cloned);
|
|
825
844
|
}
|
|
845
|
+
/**
|
|
846
|
+
* Throw `ALREADY_EXISTS` if any record other than `excludeId` already holds
|
|
847
|
+
* one of `rec`'s handle-column values — the in-memory mirror of the
|
|
848
|
+
* atscript-db store's unique index, so `create` and `update` enforce the same
|
|
849
|
+
* collision contract (which `promote-to-handle` swallows best-effort). Handle
|
|
850
|
+
* values are string columns; a non-string is never a collision.
|
|
851
|
+
*/
|
|
852
|
+
assertHandlesUnique(rec, excludeId) {
|
|
853
|
+
for (const field of this.handleFields) {
|
|
854
|
+
const value = rec[field];
|
|
855
|
+
if (typeof value !== "string") continue;
|
|
856
|
+
for (const [otherId, other] of this.store) {
|
|
857
|
+
if (otherId === excludeId) continue;
|
|
858
|
+
if (other[field] === value) throw new require_federated_identity_store.UserAuthError("ALREADY_EXISTS", `${field} "${value}" already exists`);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
}
|
|
826
862
|
async update(id, update) {
|
|
827
863
|
const user = this.store.get(id);
|
|
828
864
|
if (!user) return false;
|
|
829
865
|
if (update.expectedVersion !== void 0 && (user.version ?? 0) !== update.expectedVersion) return false;
|
|
830
|
-
if (update.set)
|
|
866
|
+
if (update.set) {
|
|
867
|
+
this.assertHandlesUnique(update.set, id);
|
|
868
|
+
require_federated_identity_store.deepMerge(user, update.set);
|
|
869
|
+
}
|
|
831
870
|
if (update.inc) for (const [path, amount] of Object.entries(update.inc)) require_federated_identity_store.incrementAtPath(user, path, amount);
|
|
832
871
|
user.version = (user.version ?? 0) + 1;
|
|
833
872
|
return true;
|
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { C as TotpConfig, D as UserCredentials, E as UserAuthErrorType, O as UserServiceConfig, S as PolicyCheckResult, T as TrustedDeviceRecord, _ as PasswordData, a as pickDefinedProfile, b as PasswordPolicyEvalFn, c as AccountData, d as LockoutConfig, f as LoginResult, g as PasswordConfig, h as MfaMethodInfo, i as NewFederatedIdentity, k as UserStoreUpdate, l as DeepPartial, m as MfaMethod, n as FederatedIdentityStore, o as UserStore, p as MfaData, r as FederatedProfileSnapshot, s as WithCasOptions, t as FederatedIdentity, u as LockStatus, v as PasswordPolicyContext, w as TransferablePolicy, x as PasswordPolicyInstance, y as PasswordPolicyDef } from "./federated-identity-store-
|
|
1
|
+
import { C as TotpConfig, D as UserCredentials, E as UserAuthErrorType, O as UserServiceConfig, S as PolicyCheckResult, T as TrustedDeviceRecord, _ as PasswordData, a as pickDefinedProfile, b as PasswordPolicyEvalFn, c as AccountData, d as LockoutConfig, f as LoginResult, g as PasswordConfig, h as MfaMethodInfo, i as NewFederatedIdentity, k as UserStoreUpdate, l as DeepPartial, m as MfaMethod, n as FederatedIdentityStore, o as UserStore, p as MfaData, r as FederatedProfileSnapshot, s as WithCasOptions, t as FederatedIdentity, u as LockStatus, v as PasswordPolicyContext, w as TransferablePolicy, x as PasswordPolicyInstance, y as PasswordPolicyDef } from "./federated-identity-store-CI7Vgllp.cjs";
|
|
2
2
|
|
|
3
3
|
//#region src/errors.d.ts
|
|
4
4
|
declare class UserAuthError extends Error {
|
|
@@ -238,17 +238,38 @@ declare class UserService<T extends object = object> {
|
|
|
238
238
|
declare class UserStoreMemory<T extends object = object> extends UserStore<T> {
|
|
239
239
|
/** Keyed by the stable surrogate `id` (the token subject). */
|
|
240
240
|
private store;
|
|
241
|
+
/**
|
|
242
|
+
* Ordered secondary handle fields (e.g. email then phone) resolved from the
|
|
243
|
+
* model's `@aooth.user.*` annotations by the wiring layer (`handleFields` of
|
|
244
|
+
* `getAoothUserHandleSpec`). Tried by `findByHandle` after `username`, and
|
|
245
|
+
* enforced unique by `create`. Empty when no handles are configured (login by
|
|
246
|
+
* email/phone unavailable).
|
|
247
|
+
*/
|
|
248
|
+
private readonly handleFields;
|
|
241
249
|
/**
|
|
242
250
|
* Optional seed. The map is keyed by each record's `id`; a record missing an
|
|
243
251
|
* `id` gets one minted (mirrors the DB `@db.default.uuid`). The seed object's
|
|
244
252
|
* keys are ignored — identity is the record's `id`.
|
|
253
|
+
*
|
|
254
|
+
* `opts.handleFields` names the ordered secondary login handles (mirroring
|
|
255
|
+
* `UsersStoreAtscriptDb`); omit it for username-only resolution.
|
|
245
256
|
*/
|
|
246
|
-
constructor(seed?: Record<string, UserCredentials & T
|
|
257
|
+
constructor(seed?: Record<string, UserCredentials & T>, opts?: {
|
|
258
|
+
handleFields?: string[];
|
|
259
|
+
});
|
|
247
260
|
exists(handle: string): Promise<boolean>;
|
|
248
261
|
findById(id: string): Promise<(UserCredentials & T) | null>;
|
|
249
262
|
findByHandle(handle: string): Promise<(UserCredentials & T) | null>;
|
|
250
263
|
findByIdentifier(value: string): Promise<(UserCredentials & T) | null>;
|
|
251
264
|
create(data: UserCredentials & T): Promise<void>;
|
|
265
|
+
/**
|
|
266
|
+
* Throw `ALREADY_EXISTS` if any record other than `excludeId` already holds
|
|
267
|
+
* one of `rec`'s handle-column values — the in-memory mirror of the
|
|
268
|
+
* atscript-db store's unique index, so `create` and `update` enforce the same
|
|
269
|
+
* collision contract (which `promote-to-handle` swallows best-effort). Handle
|
|
270
|
+
* values are string columns; a non-string is never a collision.
|
|
271
|
+
*/
|
|
272
|
+
private assertHandlesUnique;
|
|
252
273
|
update(id: string, update: UserStoreUpdate): Promise<boolean>;
|
|
253
274
|
delete(id: string): Promise<boolean>;
|
|
254
275
|
withCas(id: string, mutator: (current: UserCredentials & T) => UserStoreUpdate | null, opts?: WithCasOptions): Promise<void>;
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { C as TotpConfig, D as UserCredentials, E as UserAuthErrorType, O as UserServiceConfig, S as PolicyCheckResult, T as TrustedDeviceRecord, _ as PasswordData, a as pickDefinedProfile, b as PasswordPolicyEvalFn, c as AccountData, d as LockoutConfig, f as LoginResult, g as PasswordConfig, h as MfaMethodInfo, i as NewFederatedIdentity, k as UserStoreUpdate, l as DeepPartial, m as MfaMethod, n as FederatedIdentityStore, o as UserStore, p as MfaData, r as FederatedProfileSnapshot, s as WithCasOptions, t as FederatedIdentity, u as LockStatus, v as PasswordPolicyContext, w as TransferablePolicy, x as PasswordPolicyInstance, y as PasswordPolicyDef } from "./federated-identity-store-
|
|
1
|
+
import { C as TotpConfig, D as UserCredentials, E as UserAuthErrorType, O as UserServiceConfig, S as PolicyCheckResult, T as TrustedDeviceRecord, _ as PasswordData, a as pickDefinedProfile, b as PasswordPolicyEvalFn, c as AccountData, d as LockoutConfig, f as LoginResult, g as PasswordConfig, h as MfaMethodInfo, i as NewFederatedIdentity, k as UserStoreUpdate, l as DeepPartial, m as MfaMethod, n as FederatedIdentityStore, o as UserStore, p as MfaData, r as FederatedProfileSnapshot, s as WithCasOptions, t as FederatedIdentity, u as LockStatus, v as PasswordPolicyContext, w as TransferablePolicy, x as PasswordPolicyInstance, y as PasswordPolicyDef } from "./federated-identity-store-CI7Vgllp.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/errors.d.ts
|
|
4
4
|
declare class UserAuthError extends Error {
|
|
@@ -238,17 +238,38 @@ declare class UserService<T extends object = object> {
|
|
|
238
238
|
declare class UserStoreMemory<T extends object = object> extends UserStore<T> {
|
|
239
239
|
/** Keyed by the stable surrogate `id` (the token subject). */
|
|
240
240
|
private store;
|
|
241
|
+
/**
|
|
242
|
+
* Ordered secondary handle fields (e.g. email then phone) resolved from the
|
|
243
|
+
* model's `@aooth.user.*` annotations by the wiring layer (`handleFields` of
|
|
244
|
+
* `getAoothUserHandleSpec`). Tried by `findByHandle` after `username`, and
|
|
245
|
+
* enforced unique by `create`. Empty when no handles are configured (login by
|
|
246
|
+
* email/phone unavailable).
|
|
247
|
+
*/
|
|
248
|
+
private readonly handleFields;
|
|
241
249
|
/**
|
|
242
250
|
* Optional seed. The map is keyed by each record's `id`; a record missing an
|
|
243
251
|
* `id` gets one minted (mirrors the DB `@db.default.uuid`). The seed object's
|
|
244
252
|
* keys are ignored — identity is the record's `id`.
|
|
253
|
+
*
|
|
254
|
+
* `opts.handleFields` names the ordered secondary login handles (mirroring
|
|
255
|
+
* `UsersStoreAtscriptDb`); omit it for username-only resolution.
|
|
245
256
|
*/
|
|
246
|
-
constructor(seed?: Record<string, UserCredentials & T
|
|
257
|
+
constructor(seed?: Record<string, UserCredentials & T>, opts?: {
|
|
258
|
+
handleFields?: string[];
|
|
259
|
+
});
|
|
247
260
|
exists(handle: string): Promise<boolean>;
|
|
248
261
|
findById(id: string): Promise<(UserCredentials & T) | null>;
|
|
249
262
|
findByHandle(handle: string): Promise<(UserCredentials & T) | null>;
|
|
250
263
|
findByIdentifier(value: string): Promise<(UserCredentials & T) | null>;
|
|
251
264
|
create(data: UserCredentials & T): Promise<void>;
|
|
265
|
+
/**
|
|
266
|
+
* Throw `ALREADY_EXISTS` if any record other than `excludeId` already holds
|
|
267
|
+
* one of `rec`'s handle-column values — the in-memory mirror of the
|
|
268
|
+
* atscript-db store's unique index, so `create` and `update` enforce the same
|
|
269
|
+
* collision contract (which `promote-to-handle` swallows best-effort). Handle
|
|
270
|
+
* values are string columns; a non-string is never a collision.
|
|
271
|
+
*/
|
|
272
|
+
private assertHandlesUnique;
|
|
252
273
|
update(id: string, update: UserStoreUpdate): Promise<boolean>;
|
|
253
274
|
delete(id: string): Promise<boolean>;
|
|
254
275
|
withCas(id: string, mutator: (current: UserCredentials & T) => UserStoreUpdate | null, opts?: WithCasOptions): Promise<void>;
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as generateSecureRandom, c as maskMfaValue, d as UserAuthError, i as deepMerge, l as maskPhone, n as pickDefinedProfile, o as incrementAtPath, r as UserStore, s as maskEmail, t as FederatedIdentityStore, u as setAtPath } from "./federated-identity-store-
|
|
1
|
+
import { a as generateSecureRandom, c as maskMfaValue, d as UserAuthError, i as deepMerge, l as maskPhone, n as pickDefinedProfile, o as incrementAtPath, r as UserStore, s as maskEmail, t as FederatedIdentityStore, u as setAtPath } from "./federated-identity-store-CHW1xtMp.mjs";
|
|
2
2
|
import { createHash, createHmac, randomBytes, randomUUID, scrypt, timingSafeEqual } from "node:crypto";
|
|
3
3
|
//#region src/base-x/base32.ts
|
|
4
4
|
/**
|
|
@@ -779,12 +779,24 @@ var UserStoreMemory = class extends UserStore {
|
|
|
779
779
|
/** Keyed by the stable surrogate `id` (the token subject). */
|
|
780
780
|
store = /* @__PURE__ */ new Map();
|
|
781
781
|
/**
|
|
782
|
+
* Ordered secondary handle fields (e.g. email then phone) resolved from the
|
|
783
|
+
* model's `@aooth.user.*` annotations by the wiring layer (`handleFields` of
|
|
784
|
+
* `getAoothUserHandleSpec`). Tried by `findByHandle` after `username`, and
|
|
785
|
+
* enforced unique by `create`. Empty when no handles are configured (login by
|
|
786
|
+
* email/phone unavailable).
|
|
787
|
+
*/
|
|
788
|
+
handleFields;
|
|
789
|
+
/**
|
|
782
790
|
* Optional seed. The map is keyed by each record's `id`; a record missing an
|
|
783
791
|
* `id` gets one minted (mirrors the DB `@db.default.uuid`). The seed object's
|
|
784
792
|
* keys are ignored — identity is the record's `id`.
|
|
793
|
+
*
|
|
794
|
+
* `opts.handleFields` names the ordered secondary login handles (mirroring
|
|
795
|
+
* `UsersStoreAtscriptDb`); omit it for username-only resolution.
|
|
785
796
|
*/
|
|
786
|
-
constructor(seed) {
|
|
797
|
+
constructor(seed, opts) {
|
|
787
798
|
super();
|
|
799
|
+
this.handleFields = opts?.handleFields ?? [];
|
|
788
800
|
if (seed) for (const value of Object.values(seed)) {
|
|
789
801
|
const cloned = structuredClone(value);
|
|
790
802
|
if (!cloned.id) cloned.id = randomUUID();
|
|
@@ -800,12 +812,21 @@ var UserStoreMemory = class extends UserStore {
|
|
|
800
812
|
return user ? structuredClone(user) : null;
|
|
801
813
|
}
|
|
802
814
|
async findByHandle(handle) {
|
|
803
|
-
let
|
|
815
|
+
let byHandle = null;
|
|
804
816
|
for (const u of this.store.values()) {
|
|
805
817
|
if (u.username === handle) return structuredClone(u);
|
|
806
|
-
if (
|
|
818
|
+
if (byHandle === null) {
|
|
819
|
+
const rec = u;
|
|
820
|
+
for (const field of this.handleFields) {
|
|
821
|
+
const value = rec[field];
|
|
822
|
+
if (value !== void 0 && value === handle) {
|
|
823
|
+
byHandle = u;
|
|
824
|
+
break;
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
}
|
|
807
828
|
}
|
|
808
|
-
return
|
|
829
|
+
return byHandle ? structuredClone(byHandle) : null;
|
|
809
830
|
}
|
|
810
831
|
async findByIdentifier(value) {
|
|
811
832
|
const byId = this.store.get(value);
|
|
@@ -815,18 +836,36 @@ var UserStoreMemory = class extends UserStore {
|
|
|
815
836
|
async create(data) {
|
|
816
837
|
const cloned = structuredClone(data);
|
|
817
838
|
if (!cloned.id) cloned.id = randomUUID();
|
|
818
|
-
for (const u of this.store.values()) {
|
|
819
|
-
|
|
820
|
-
if (cloned.email !== void 0 && u.email === cloned.email) throw new UserAuthError("ALREADY_EXISTS", `Email "${cloned.email}" already exists`);
|
|
821
|
-
}
|
|
839
|
+
for (const u of this.store.values()) if (u.username === cloned.username) throw new UserAuthError("ALREADY_EXISTS", `User "${cloned.username}" already exists`);
|
|
840
|
+
this.assertHandlesUnique(cloned);
|
|
822
841
|
cloned.version = 0;
|
|
823
842
|
this.store.set(cloned.id, cloned);
|
|
824
843
|
}
|
|
844
|
+
/**
|
|
845
|
+
* Throw `ALREADY_EXISTS` if any record other than `excludeId` already holds
|
|
846
|
+
* one of `rec`'s handle-column values — the in-memory mirror of the
|
|
847
|
+
* atscript-db store's unique index, so `create` and `update` enforce the same
|
|
848
|
+
* collision contract (which `promote-to-handle` swallows best-effort). Handle
|
|
849
|
+
* values are string columns; a non-string is never a collision.
|
|
850
|
+
*/
|
|
851
|
+
assertHandlesUnique(rec, excludeId) {
|
|
852
|
+
for (const field of this.handleFields) {
|
|
853
|
+
const value = rec[field];
|
|
854
|
+
if (typeof value !== "string") continue;
|
|
855
|
+
for (const [otherId, other] of this.store) {
|
|
856
|
+
if (otherId === excludeId) continue;
|
|
857
|
+
if (other[field] === value) throw new UserAuthError("ALREADY_EXISTS", `${field} "${value}" already exists`);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
}
|
|
825
861
|
async update(id, update) {
|
|
826
862
|
const user = this.store.get(id);
|
|
827
863
|
if (!user) return false;
|
|
828
864
|
if (update.expectedVersion !== void 0 && (user.version ?? 0) !== update.expectedVersion) return false;
|
|
829
|
-
if (update.set)
|
|
865
|
+
if (update.set) {
|
|
866
|
+
this.assertHandlesUnique(update.set, id);
|
|
867
|
+
deepMerge(user, update.set);
|
|
868
|
+
}
|
|
830
869
|
if (update.inc) for (const [path, amount] of Object.entries(update.inc)) incrementAtPath(user, path, amount);
|
|
831
870
|
user.version = (user.version ?? 0) + 1;
|
|
832
871
|
return true;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aooth/user",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"description": "User credential primitives for aoothjs",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"aoothjs",
|
|
@@ -60,14 +60,14 @@
|
|
|
60
60
|
"access": "public"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
|
-
"@atscript/core": "^0.1.
|
|
64
|
-
"@atscript/db": "^0.1.
|
|
65
|
-
"@atscript/db-sql-tools": "^0.1.
|
|
66
|
-
"@atscript/db-sqlite": "^0.1.
|
|
67
|
-
"@atscript/typescript": "^0.1.
|
|
63
|
+
"@atscript/core": "^0.1.71",
|
|
64
|
+
"@atscript/db": "^0.1.98",
|
|
65
|
+
"@atscript/db-sql-tools": "^0.1.98",
|
|
66
|
+
"@atscript/db-sqlite": "^0.1.98",
|
|
67
|
+
"@atscript/typescript": "^0.1.71",
|
|
68
68
|
"@types/better-sqlite3": "^7.6.13",
|
|
69
69
|
"better-sqlite3": "^12.6.2",
|
|
70
|
-
"unplugin-atscript": "^0.1.
|
|
70
|
+
"unplugin-atscript": "^0.1.71"
|
|
71
71
|
},
|
|
72
72
|
"peerDependencies": {
|
|
73
73
|
"@atscript/db": ">=0.1.79"
|