@sasjs/api 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -8
- package/build/browser.js +788 -4
- package/build/cjs/corpus/identity.js +268 -0
- package/build/cjs/corpus/runner.js +11 -2
- package/build/cjs/host/dispatch.js +57 -2
- package/build/cjs/identity/fixture.js +118 -0
- package/build/cjs/identity/memory-store.js +179 -0
- package/build/cjs/identity/service.js +142 -0
- package/build/cjs/identity/types.js +11 -0
- package/build/cjs/index.js +11 -1
- package/build/cjs/routes/generated.js +72 -0
- package/build/cjs/routes/index.js +6 -0
- package/build/cjs/routes/overlay.js +38 -0
- package/build/esm/corpus/drive.d.ts +14 -0
- package/build/esm/corpus/drive.d.ts.map +1 -1
- package/build/esm/corpus/identity.d.ts +17 -0
- package/build/esm/corpus/identity.d.ts.map +1 -0
- package/build/esm/corpus/identity.js +265 -0
- package/build/esm/corpus/runner.d.ts +6 -1
- package/build/esm/corpus/runner.d.ts.map +1 -1
- package/build/esm/corpus/runner.js +11 -2
- package/build/esm/host/dispatch.d.ts +16 -0
- package/build/esm/host/dispatch.d.ts.map +1 -1
- package/build/esm/host/dispatch.js +57 -2
- package/build/esm/identity/fixture.d.ts +84 -0
- package/build/esm/identity/fixture.d.ts.map +1 -0
- package/build/esm/identity/fixture.js +114 -0
- package/build/esm/identity/memory-store.d.ts +51 -0
- package/build/esm/identity/memory-store.d.ts.map +1 -0
- package/build/esm/identity/memory-store.js +175 -0
- package/build/esm/identity/service.d.ts +48 -0
- package/build/esm/identity/service.d.ts.map +1 -0
- package/build/esm/identity/service.js +137 -0
- package/build/esm/identity/types.d.ts +79 -0
- package/build/esm/identity/types.d.ts.map +1 -0
- package/build/esm/identity/types.js +10 -0
- package/build/esm/index.d.ts +7 -0
- package/build/esm/index.d.ts.map +1 -1
- package/build/esm/index.js +4 -0
- package/build/esm/routes/generated.d.ts.map +1 -1
- package/build/esm/routes/generated.js +72 -0
- package/build/esm/routes/index.d.ts.map +1 -1
- package/build/esm/routes/index.js +6 -0
- package/build/esm/routes/overlay.d.ts +9 -0
- package/build/esm/routes/overlay.d.ts.map +1 -1
- package/build/esm/routes/overlay.js +38 -0
- package/build/esm/types.d.ts +7 -0
- package/build/esm/types.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An `IdentityStore` over maps, for tests and for a host with no database.
|
|
3
|
+
*
|
|
4
|
+
* Tokens are opaque strings this store can look up, which is the shape the
|
|
5
|
+
* browser build needs: no signing key, and nothing in the string a client can
|
|
6
|
+
* read. They are minted from the platform's cryptographic source and carry the
|
|
7
|
+
* client's lifetime, so a guessed string buys nothing and an old one stops
|
|
8
|
+
* resolving. A host with real credentials - a server - supplies its own store
|
|
9
|
+
* with JWTs, bcrypt and a database behind it.
|
|
10
|
+
*/
|
|
11
|
+
import type { IdentityClient, IdentityStore, IdentityUser, Subject, TokenPair } from './types.js';
|
|
12
|
+
import { type IdentityFixture } from './fixture.js';
|
|
13
|
+
/**
|
|
14
|
+
* Whether a token minted for one subject names the same user and client.
|
|
15
|
+
*
|
|
16
|
+
* The fixture can declare a token that outlives the map it is seeded into, so
|
|
17
|
+
* the read paths compare rather than trust the reference.
|
|
18
|
+
*/
|
|
19
|
+
export declare class MemoryIdentityStore implements IdentityStore {
|
|
20
|
+
private readonly users;
|
|
21
|
+
private readonly passwords;
|
|
22
|
+
private readonly clients;
|
|
23
|
+
private readonly pairs;
|
|
24
|
+
private readonly access;
|
|
25
|
+
private readonly refresh;
|
|
26
|
+
private readonly codes;
|
|
27
|
+
private readonly claimed;
|
|
28
|
+
constructor(fixture?: IdentityFixture);
|
|
29
|
+
userByName(username: string): Promise<IdentityUser | undefined>;
|
|
30
|
+
userById(uid: string): Promise<IdentityUser | undefined>;
|
|
31
|
+
/** A fixture holds the password in clear, so the check is an equality. */
|
|
32
|
+
passwordMatches(user: IdentityUser, password: string): Promise<boolean>;
|
|
33
|
+
client(clientId: string): Promise<IdentityClient | undefined>;
|
|
34
|
+
issue(subject: Subject, client: IdentityClient): Promise<TokenPair>;
|
|
35
|
+
/**
|
|
36
|
+
* A token names its subject only while that subject's CURRENT pair holds it.
|
|
37
|
+
* The server verifies a token against the pair saved for the user and client,
|
|
38
|
+
* so a logged-out or rotated token stops working there too; without the same
|
|
39
|
+
* check here, the browser host would honour a token the server had revoked.
|
|
40
|
+
*/
|
|
41
|
+
readAccess(token: string): Promise<Subject | undefined>;
|
|
42
|
+
readRefresh(token: string): Promise<Subject | undefined>;
|
|
43
|
+
private current;
|
|
44
|
+
pairFor(subject: Subject): Promise<TokenPair | undefined>;
|
|
45
|
+
savePair(subject: Subject, pair: TokenPair): Promise<void>;
|
|
46
|
+
deletePair(subject: Subject): Promise<void>;
|
|
47
|
+
issueCode(subject: Subject): Promise<string>;
|
|
48
|
+
readCode(code: string): Promise<Subject | undefined>;
|
|
49
|
+
claimCode(code: string): Promise<boolean>;
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=memory-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"memory-store.d.ts","sourceRoot":"","sources":["../../../src/identity/memory-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,KAAK,EACV,cAAc,EACd,aAAa,EACb,YAAY,EACZ,OAAO,EACP,SAAS,EACV,MAAM,YAAY,CAAA;AACnB,OAAO,EAGL,KAAK,eAAe,EACrB,MAAM,cAAc,CAAA;AAyDrB;;;;;GAKG;AACH,qBAAa,mBAAoB,YAAW,aAAa;IACvD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAqB;IAC3C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA4B;IACtD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAoC;IAC5D,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA+B;IACrD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA4B;IACnD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA4B;IACpD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA4B;IAClD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAoB;gBAEhC,OAAO,GAAE,eAAkC;IAsDjD,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,SAAS,CAAC;IAI/D,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,SAAS,CAAC;IAI9D,0EAA0E;IACpE,eAAe,CACnB,IAAI,EAAE,YAAY,EAClB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,OAAO,CAAC;IAIb,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC;IAI7D,KAAK,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,SAAS,CAAC;IAoBzE;;;;;OAKG;IACG,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC;IAIvD,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC;IAI9D,OAAO,CAAC,OAAO;IAeT,OAAO,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC;IAIzD,QAAQ,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;IAI1D,UAAU,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAI3C,SAAS,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC;IAS5C,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC;IAIpD,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;CAKhD"}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { DEFAULT_IDENTITY, IDENTITY_FORMAT } from './fixture.js';
|
|
2
|
+
import { ApiError } from '../types.js';
|
|
3
|
+
const keyOf = (subject) => `${subject.userId}|${subject.clientId}`;
|
|
4
|
+
/**
|
|
5
|
+
* Opaque identity strings, from the platform's cryptographic source.
|
|
6
|
+
*
|
|
7
|
+
* These strings are credentials: whoever holds one is the caller it names. A
|
|
8
|
+
* counter or `Math.random` is a sequence a reader can reproduce, so it would
|
|
9
|
+
* let one caller name another - the store is small and demo-shaped, but a token
|
|
10
|
+
* that can be guessed is not a demo quirk, it is an authentication bypass.
|
|
11
|
+
*
|
|
12
|
+
* `globalThis.crypto` is present in every browser and in Node from 19, which is
|
|
13
|
+
* the same reach the rest of this package has (no node imports, so the browser
|
|
14
|
+
* build bundles it unchanged).
|
|
15
|
+
*/
|
|
16
|
+
const randomHex = (bytes) => {
|
|
17
|
+
const source = globalThis.crypto;
|
|
18
|
+
if (!source)
|
|
19
|
+
throw new ApiError(500, 'This host has no Web Crypto (`globalThis.crypto`), so it cannot mint an unguessable identity string.');
|
|
20
|
+
const buffer = new Uint8Array(bytes);
|
|
21
|
+
source.getRandomValues(buffer);
|
|
22
|
+
return Array.from(buffer, (byte) => byte.toString(16).padStart(2, '0')).join('');
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* How long a code buys tokens, in seconds.
|
|
26
|
+
*
|
|
27
|
+
* The same lifetime `sasjs/server` signs into its auth code JWT, so a code a
|
|
28
|
+
* mock issues and one a server issues age out together.
|
|
29
|
+
*/
|
|
30
|
+
const CODE_LIFETIME_SECONDS = 30;
|
|
31
|
+
const live = (issued, now) => issued && (issued.expiresAt === undefined || issued.expiresAt > now)
|
|
32
|
+
? issued.subject
|
|
33
|
+
: undefined;
|
|
34
|
+
/**
|
|
35
|
+
* Whether a token minted for one subject names the same user and client.
|
|
36
|
+
*
|
|
37
|
+
* The fixture can declare a token that outlives the map it is seeded into, so
|
|
38
|
+
* the read paths compare rather than trust the reference.
|
|
39
|
+
*/
|
|
40
|
+
export class MemoryIdentityStore {
|
|
41
|
+
constructor(fixture = DEFAULT_IDENTITY) {
|
|
42
|
+
this.users = [];
|
|
43
|
+
this.passwords = new Map();
|
|
44
|
+
this.clients = new Map();
|
|
45
|
+
this.pairs = new Map();
|
|
46
|
+
this.access = new Map();
|
|
47
|
+
this.refresh = new Map();
|
|
48
|
+
this.codes = new Map();
|
|
49
|
+
this.claimed = new Set();
|
|
50
|
+
if (fixture.format !== IDENTITY_FORMAT)
|
|
51
|
+
throw new ApiError(500, `The identity fixture states format ${String(fixture.format)}; this store reads ${IDENTITY_FORMAT}.`);
|
|
52
|
+
for (const user of fixture.users) {
|
|
53
|
+
this.users.push({
|
|
54
|
+
uid: user.uid,
|
|
55
|
+
username: user.username,
|
|
56
|
+
displayName: user.displayName,
|
|
57
|
+
isAdmin: user.isAdmin,
|
|
58
|
+
isActive: user.isActive,
|
|
59
|
+
needsToUpdatePassword: user.needsToUpdatePassword
|
|
60
|
+
});
|
|
61
|
+
this.passwords.set(user.uid, user.password);
|
|
62
|
+
}
|
|
63
|
+
for (const client of fixture.clients)
|
|
64
|
+
this.clients.set(client.clientId, {
|
|
65
|
+
clientId: client.clientId,
|
|
66
|
+
accessTokenExpiration: client.accessTokenExpiration,
|
|
67
|
+
refreshTokenExpiration: client.refreshTokenExpiration
|
|
68
|
+
});
|
|
69
|
+
for (const token of fixture.tokens || []) {
|
|
70
|
+
const user = this.users.find((candidate) => candidate.username === token.username);
|
|
71
|
+
if (!user)
|
|
72
|
+
continue;
|
|
73
|
+
const subject = { userId: user.uid, clientId: token.clientId };
|
|
74
|
+
if (token.accessToken)
|
|
75
|
+
this.access.set(token.accessToken, { subject });
|
|
76
|
+
if (token.refreshToken)
|
|
77
|
+
this.refresh.set(token.refreshToken, { subject });
|
|
78
|
+
// a seeded token is the subject's live pair, which is what makes it
|
|
79
|
+
// authenticate: the read paths check the pair, not the token alone
|
|
80
|
+
if (token.accessToken && token.refreshToken)
|
|
81
|
+
this.pairs.set(keyOf(subject), {
|
|
82
|
+
accessToken: token.accessToken,
|
|
83
|
+
refreshToken: token.refreshToken
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
for (const code of fixture.codes || []) {
|
|
87
|
+
const user = this.users.find((candidate) => candidate.username === code.username);
|
|
88
|
+
if (!user)
|
|
89
|
+
continue;
|
|
90
|
+
this.codes.set(code.code, {
|
|
91
|
+
subject: { userId: user.uid, clientId: code.clientId }
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
async userByName(username) {
|
|
96
|
+
return this.users.find((user) => user.username === username);
|
|
97
|
+
}
|
|
98
|
+
async userById(uid) {
|
|
99
|
+
return this.users.find((user) => user.uid === uid);
|
|
100
|
+
}
|
|
101
|
+
/** A fixture holds the password in clear, so the check is an equality. */
|
|
102
|
+
async passwordMatches(user, password) {
|
|
103
|
+
return this.passwords.get(user.uid) === password;
|
|
104
|
+
}
|
|
105
|
+
async client(clientId) {
|
|
106
|
+
return this.clients.get(clientId);
|
|
107
|
+
}
|
|
108
|
+
async issue(subject, client) {
|
|
109
|
+
// The lifetimes are the client's, recorded against the instant of issue: a
|
|
110
|
+
// token's expiry is a fact about this pair, so the read paths can answer
|
|
111
|
+
// "invalid or expired" rather than carrying a number nothing compares.
|
|
112
|
+
const issuedAt = Date.now();
|
|
113
|
+
const pair = {
|
|
114
|
+
accessToken: `access-${randomHex(16)}`,
|
|
115
|
+
refreshToken: `refresh-${randomHex(16)}`
|
|
116
|
+
};
|
|
117
|
+
this.access.set(pair.accessToken, {
|
|
118
|
+
subject,
|
|
119
|
+
expiresAt: issuedAt + client.accessTokenExpiration * 1000
|
|
120
|
+
});
|
|
121
|
+
this.refresh.set(pair.refreshToken, {
|
|
122
|
+
subject,
|
|
123
|
+
expiresAt: issuedAt + client.refreshTokenExpiration * 1000
|
|
124
|
+
});
|
|
125
|
+
return pair;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* A token names its subject only while that subject's CURRENT pair holds it.
|
|
129
|
+
* The server verifies a token against the pair saved for the user and client,
|
|
130
|
+
* so a logged-out or rotated token stops working there too; without the same
|
|
131
|
+
* check here, the browser host would honour a token the server had revoked.
|
|
132
|
+
*/
|
|
133
|
+
async readAccess(token) {
|
|
134
|
+
return this.current(token, this.access, 'accessToken');
|
|
135
|
+
}
|
|
136
|
+
async readRefresh(token) {
|
|
137
|
+
return this.current(token, this.refresh, 'refreshToken');
|
|
138
|
+
}
|
|
139
|
+
current(token, issued, kind) {
|
|
140
|
+
const entry = issued.get(token);
|
|
141
|
+
if (!entry)
|
|
142
|
+
return undefined;
|
|
143
|
+
const subject = live(entry, Date.now());
|
|
144
|
+
if (!subject)
|
|
145
|
+
return undefined;
|
|
146
|
+
const pair = this.pairs.get(keyOf(subject));
|
|
147
|
+
return pair && pair[kind] === token ? subject : undefined;
|
|
148
|
+
}
|
|
149
|
+
async pairFor(subject) {
|
|
150
|
+
return this.pairs.get(keyOf(subject));
|
|
151
|
+
}
|
|
152
|
+
async savePair(subject, pair) {
|
|
153
|
+
this.pairs.set(keyOf(subject), pair);
|
|
154
|
+
}
|
|
155
|
+
async deletePair(subject) {
|
|
156
|
+
this.pairs.delete(keyOf(subject));
|
|
157
|
+
}
|
|
158
|
+
async issueCode(subject) {
|
|
159
|
+
const code = `code-${randomHex(16)}`;
|
|
160
|
+
this.codes.set(code, {
|
|
161
|
+
subject,
|
|
162
|
+
expiresAt: Date.now() + CODE_LIFETIME_SECONDS * 1000
|
|
163
|
+
});
|
|
164
|
+
return code;
|
|
165
|
+
}
|
|
166
|
+
async readCode(code) {
|
|
167
|
+
return live(this.codes.get(code), Date.now());
|
|
168
|
+
}
|
|
169
|
+
async claimCode(code) {
|
|
170
|
+
if (this.claimed.has(code) || !this.codes.has(code))
|
|
171
|
+
return false;
|
|
172
|
+
this.claimed.add(code);
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { IdentityStore, IdentityUser, Subject, TokenPair } from './types.js';
|
|
2
|
+
export interface LoginResult {
|
|
3
|
+
loggedIn: boolean;
|
|
4
|
+
user: {
|
|
5
|
+
uid: string;
|
|
6
|
+
username: string;
|
|
7
|
+
displayName: string;
|
|
8
|
+
isAdmin: boolean;
|
|
9
|
+
needsToUpdatePassword: boolean;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export interface SessionResult {
|
|
13
|
+
uid: string;
|
|
14
|
+
username: string;
|
|
15
|
+
displayName: string;
|
|
16
|
+
isAdmin: boolean;
|
|
17
|
+
needsToUpdatePassword: boolean;
|
|
18
|
+
}
|
|
19
|
+
export interface IdentityService {
|
|
20
|
+
/**
|
|
21
|
+
* The subject an access token names, or undefined. A host that already knows
|
|
22
|
+
* its caller - a session cookie - does not need this.
|
|
23
|
+
*/
|
|
24
|
+
authenticate(token: string | undefined): Promise<Subject | undefined>;
|
|
25
|
+
login(credentials: {
|
|
26
|
+
username: string;
|
|
27
|
+
password: string;
|
|
28
|
+
}): Promise<LoginResult>;
|
|
29
|
+
/** The authorization code a signed-in caller exchanges for tokens. */
|
|
30
|
+
authorize(subject: Subject | undefined, clientId: string): Promise<string>;
|
|
31
|
+
/** The token pair an authorization code buys. */
|
|
32
|
+
token(request: {
|
|
33
|
+
clientId: string;
|
|
34
|
+
code: string;
|
|
35
|
+
}): Promise<TokenPair>;
|
|
36
|
+
/** A new pair for the caller a refresh token names. */
|
|
37
|
+
refresh(token: string | undefined): Promise<TokenPair>;
|
|
38
|
+
/** Revokes the caller's pair. Answers nothing; the route is a 204. */
|
|
39
|
+
logout(subject: Subject | undefined): Promise<void>;
|
|
40
|
+
/** The body the browser sign-out answers once the host has reset its session. */
|
|
41
|
+
logoutSession(): Promise<string>;
|
|
42
|
+
/** The caller, as `GET /SASjsApi/session` reports it. */
|
|
43
|
+
session(subject: Subject | undefined): Promise<SessionResult>;
|
|
44
|
+
}
|
|
45
|
+
export declare const createIdentityService: (store: IdentityStore) => IdentityService;
|
|
46
|
+
/** A subject, as the session route reports it. */
|
|
47
|
+
export declare const asSession: (user: IdentityUser) => SessionResult;
|
|
48
|
+
//# sourceMappingURL=service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../../src/identity/service.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EACV,aAAa,EACb,YAAY,EACZ,OAAO,EACP,SAAS,EACV,MAAM,YAAY,CAAA;AAEnB,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,OAAO,CAAA;IACjB,IAAI,EAAE;QACJ,GAAG,EAAE,MAAM,CAAA;QACX,QAAQ,EAAE,MAAM,CAAA;QAChB,WAAW,EAAE,MAAM,CAAA;QACnB,OAAO,EAAE,OAAO,CAAA;QAChB,qBAAqB,EAAE,OAAO,CAAA;KAC/B,CAAA;CACF;AAED,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAA;IACX,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;IACnB,OAAO,EAAE,OAAO,CAAA;IAChB,qBAAqB,EAAE,OAAO,CAAA;CAC/B;AAED,MAAM,WAAW,eAAe;IAC9B;;;OAGG;IACH,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC,CAAA;IACrE,KAAK,CAAC,WAAW,EAAE;QACjB,QAAQ,EAAE,MAAM,CAAA;QAChB,QAAQ,EAAE,MAAM,CAAA;KACjB,GAAG,OAAO,CAAC,WAAW,CAAC,CAAA;IACxB,sEAAsE;IACtE,SAAS,CAAC,OAAO,EAAE,OAAO,GAAG,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;IAC1E,iDAAiD;IACjD,KAAK,CAAC,OAAO,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,SAAS,CAAC,CAAA;IACtE,uDAAuD;IACvD,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC,CAAA;IACtD,sEAAsE;IACtE,MAAM,CAAC,OAAO,EAAE,OAAO,GAAG,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACnD,iFAAiF;IACjF,aAAa,IAAI,OAAO,CAAC,MAAM,CAAC,CAAA;IAChC,yDAAyD;IACzD,OAAO,CAAC,OAAO,EAAE,OAAO,GAAG,SAAS,GAAG,OAAO,CAAC,aAAa,CAAC,CAAA;CAC9D;AAsBD,eAAO,MAAM,qBAAqB,GAChC,OAAO,aAAa,KACnB,eAiHD,CAAA;AAEF,kDAAkD;AAClD,eAAO,MAAM,SAAS,GAAI,MAAM,YAAY,KAAG,aAM7C,CAAA"}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The sign-in flow's decisions: what each route answers, in which order, with
|
|
3
|
+
* which wording.
|
|
4
|
+
*
|
|
5
|
+
* The transport stays with the host - a session cookie and a CSRF token on a
|
|
6
|
+
* server, whatever the browser build holds - so nothing here reads a request.
|
|
7
|
+
* A host resolves the caller (a bearer token through `authenticate`, a session
|
|
8
|
+
* it already has) and passes the subject in; the service decides whether that
|
|
9
|
+
* is enough for the operation to proceed.
|
|
10
|
+
*/
|
|
11
|
+
import { ApiError } from '../types.js';
|
|
12
|
+
const unauthorized = () => new ApiError(401, 'Unauthorized');
|
|
13
|
+
/**
|
|
14
|
+
* The subject, provided the account behind it may still hold a token.
|
|
15
|
+
*
|
|
16
|
+
* `authenticate` reads a token on every request and refuses a deactivated
|
|
17
|
+
* account; an exchange or a refresh that skipped the same check would mint a
|
|
18
|
+
* fresh pair for exactly that account, so the check sits beside every path that
|
|
19
|
+
* reaches `store.issue`.
|
|
20
|
+
*/
|
|
21
|
+
const liveSubject = async (store, subject) => {
|
|
22
|
+
const user = await store.userById(subject.userId);
|
|
23
|
+
if (!user || !user.isActive)
|
|
24
|
+
throw unauthorized();
|
|
25
|
+
return subject;
|
|
26
|
+
};
|
|
27
|
+
export const createIdentityService = (store) => ({
|
|
28
|
+
async authenticate(token) {
|
|
29
|
+
if (token === undefined)
|
|
30
|
+
return undefined;
|
|
31
|
+
const subject = await store.readAccess(token);
|
|
32
|
+
if (!subject)
|
|
33
|
+
return undefined;
|
|
34
|
+
// A token names a user who may since have been deactivated. sasjs/server
|
|
35
|
+
// checks `isActive` on every request, so a token that outlived the account
|
|
36
|
+
// must stop resolving here too - otherwise the browser host honours a
|
|
37
|
+
// caller the server would refuse.
|
|
38
|
+
const user = await store.userById(subject.userId);
|
|
39
|
+
return user && user.isActive ? subject : undefined;
|
|
40
|
+
},
|
|
41
|
+
async login({ username, password }) {
|
|
42
|
+
const user = await store.userByName(username);
|
|
43
|
+
// An unknown username and a wrong password answer with different messages
|
|
44
|
+
// on purpose: the server's login form tells the caller which one it was,
|
|
45
|
+
// and the account lockout - the defence against guessing - is the host's.
|
|
46
|
+
if (!user)
|
|
47
|
+
throw new ApiError(401, 'Username is not found.');
|
|
48
|
+
if (!(await store.passwordMatches(user, password)))
|
|
49
|
+
throw new ApiError(401, 'Invalid Password.');
|
|
50
|
+
// Checked after the password, so the answer is not a way to learn that an
|
|
51
|
+
// account exists but is switched off without knowing its password.
|
|
52
|
+
if (!user.isActive)
|
|
53
|
+
throw new ApiError(401, 'This account is not active.');
|
|
54
|
+
return {
|
|
55
|
+
loggedIn: true,
|
|
56
|
+
user: {
|
|
57
|
+
uid: user.uid,
|
|
58
|
+
username: user.username,
|
|
59
|
+
displayName: user.displayName,
|
|
60
|
+
isAdmin: user.isAdmin,
|
|
61
|
+
needsToUpdatePassword: user.needsToUpdatePassword
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
},
|
|
65
|
+
async authorize(subject, clientId) {
|
|
66
|
+
// The same answer the server's middleware gives a caller with no token: the
|
|
67
|
+
// two hosts must not describe the same refusal differently.
|
|
68
|
+
if (!subject)
|
|
69
|
+
throw unauthorized();
|
|
70
|
+
if (!(await store.client(clientId)))
|
|
71
|
+
throw new ApiError(403, 'Invalid clientId.');
|
|
72
|
+
return store.issueCode({ userId: subject.userId, clientId });
|
|
73
|
+
},
|
|
74
|
+
async token({ clientId, code }) {
|
|
75
|
+
// Read before claiming: a code presented with another client is refused
|
|
76
|
+
// with the same answer as one that does not verify, and it is not spent, so
|
|
77
|
+
// the caller can still exchange it for the client it was issued for.
|
|
78
|
+
const subject = await store.readCode(code);
|
|
79
|
+
if (!subject || subject.clientId !== clientId)
|
|
80
|
+
throw new ApiError(401, 'Invalid Auth Code');
|
|
81
|
+
// The client is read before the code is spent and before an existing pair is
|
|
82
|
+
// handed back: both the pair and the tokens it would buy belong to the
|
|
83
|
+
// client, so a client deleted since the code was issued refuses the
|
|
84
|
+
// exchange - and refuses it without consuming the code.
|
|
85
|
+
const client = await store.client(clientId);
|
|
86
|
+
if (!client)
|
|
87
|
+
throw new ApiError(401, 'Invalid clientId.');
|
|
88
|
+
const named = await liveSubject(store, subject);
|
|
89
|
+
if (!(await store.claimCode(code)))
|
|
90
|
+
throw new ApiError(401, 'Invalid Auth Code');
|
|
91
|
+
// Re-exchanging a code for the same user and client while a valid pair
|
|
92
|
+
// exists returns that pair: another tab holding the old tokens must not be
|
|
93
|
+
// signed out by a second exchange.
|
|
94
|
+
const existing = await store.pairFor(named);
|
|
95
|
+
if (existing)
|
|
96
|
+
return existing;
|
|
97
|
+
const pair = await store.issue(named, client);
|
|
98
|
+
await store.savePair(named, pair);
|
|
99
|
+
return pair;
|
|
100
|
+
},
|
|
101
|
+
async refresh(token) {
|
|
102
|
+
const subject = token === undefined ? undefined : await store.readRefresh(token);
|
|
103
|
+
if (!subject)
|
|
104
|
+
throw unauthorized();
|
|
105
|
+
const client = await store.client(subject.clientId);
|
|
106
|
+
if (!client)
|
|
107
|
+
throw new ApiError(401, 'Invalid clientId.');
|
|
108
|
+
const named = await liveSubject(store, subject);
|
|
109
|
+
const pair = await store.issue(named, client);
|
|
110
|
+
await store.savePair(named, pair);
|
|
111
|
+
return pair;
|
|
112
|
+
},
|
|
113
|
+
async logout(subject) {
|
|
114
|
+
if (!subject)
|
|
115
|
+
throw unauthorized();
|
|
116
|
+
await store.deletePair(subject);
|
|
117
|
+
},
|
|
118
|
+
async logoutSession() {
|
|
119
|
+
return 'OK!';
|
|
120
|
+
},
|
|
121
|
+
async session(subject) {
|
|
122
|
+
if (!subject)
|
|
123
|
+
throw unauthorized();
|
|
124
|
+
const user = await store.userById(subject.userId);
|
|
125
|
+
if (!user)
|
|
126
|
+
throw unauthorized();
|
|
127
|
+
return asSession(user);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
/** A subject, as the session route reports it. */
|
|
131
|
+
export const asSession = (user) => ({
|
|
132
|
+
uid: user.uid,
|
|
133
|
+
username: user.username,
|
|
134
|
+
displayName: user.displayName,
|
|
135
|
+
isAdmin: user.isAdmin,
|
|
136
|
+
needsToUpdatePassword: user.needsToUpdatePassword
|
|
137
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The identity the API works over, as a host supplies it.
|
|
3
|
+
*
|
|
4
|
+
* A server holds users in a database, verifies a bcrypt hash and signs JWTs; a
|
|
5
|
+
* browser build holds a seeded fixture, compares a plain-text demo password and
|
|
6
|
+
* mints opaque strings. Those differences are the host's, so they live behind
|
|
7
|
+
* this port - the statuses, the messages and the order of the checks are the
|
|
8
|
+
* service's, and both hosts run the same corpus over them.
|
|
9
|
+
*/
|
|
10
|
+
/** The caller a token names: a user, and the client the token was issued for. */
|
|
11
|
+
export interface Subject {
|
|
12
|
+
userId: string;
|
|
13
|
+
clientId: string;
|
|
14
|
+
}
|
|
15
|
+
export interface IdentityUser {
|
|
16
|
+
uid: string;
|
|
17
|
+
username: string;
|
|
18
|
+
displayName: string;
|
|
19
|
+
isAdmin: boolean;
|
|
20
|
+
isActive: boolean;
|
|
21
|
+
needsToUpdatePassword: boolean;
|
|
22
|
+
}
|
|
23
|
+
export interface IdentityClient {
|
|
24
|
+
clientId: string;
|
|
25
|
+
/** Seconds an access token lives, as the client is configured. */
|
|
26
|
+
accessTokenExpiration: number;
|
|
27
|
+
/** Seconds a refresh token lives. */
|
|
28
|
+
refreshTokenExpiration: number;
|
|
29
|
+
}
|
|
30
|
+
export interface TokenPair {
|
|
31
|
+
accessToken: string;
|
|
32
|
+
refreshToken: string;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Users, clients, token pairs and auth codes, plus the cryptography that names
|
|
36
|
+
* a subject in a token.
|
|
37
|
+
*
|
|
38
|
+
* Minting and reading a token sit here rather than on a separate codec because
|
|
39
|
+
* they are one decision: a host that signs JWTs also verifies them against the
|
|
40
|
+
* same secret, and a host with no signing key issues opaque strings it can look
|
|
41
|
+
* up. Splitting them would let a host pair one host's minting with another's
|
|
42
|
+
* verification, which is never what anyone means.
|
|
43
|
+
*/
|
|
44
|
+
export interface IdentityStore {
|
|
45
|
+
userByName(username: string): Promise<IdentityUser | undefined>;
|
|
46
|
+
userById(uid: string): Promise<IdentityUser | undefined>;
|
|
47
|
+
/**
|
|
48
|
+
* Whether the password matches. The host owns the check: a bcrypt hash, a
|
|
49
|
+
* directory bind, or a fixture's declared demo password.
|
|
50
|
+
*/
|
|
51
|
+
passwordMatches(user: IdentityUser, password: string): Promise<boolean>;
|
|
52
|
+
client(clientId: string): Promise<IdentityClient | undefined>;
|
|
53
|
+
/** A pair for the subject, with the client's own lifetimes. */
|
|
54
|
+
issue(subject: Subject, client: IdentityClient): Promise<TokenPair>;
|
|
55
|
+
/** The subject an access token names, or undefined when it is invalid or expired. */
|
|
56
|
+
readAccess(token: string): Promise<Subject | undefined>;
|
|
57
|
+
/** The same for a refresh token. A host may keep them in separate stores. */
|
|
58
|
+
readRefresh(token: string): Promise<Subject | undefined>;
|
|
59
|
+
/** The pair currently held for the subject, if the host keeps one. */
|
|
60
|
+
pairFor(subject: Subject): Promise<TokenPair | undefined>;
|
|
61
|
+
savePair(subject: Subject, pair: TokenPair): Promise<void>;
|
|
62
|
+
deletePair(subject: Subject): Promise<void>;
|
|
63
|
+
/** A single-use authorization code naming the subject. */
|
|
64
|
+
issueCode(subject: Subject): Promise<string>;
|
|
65
|
+
/**
|
|
66
|
+
* The subject a code names, without consuming it: authentic and unexpired,
|
|
67
|
+
* or undefined. Read first, so a code presented with the wrong client id is
|
|
68
|
+
* refused without being spent - the caller may still exchange it for the
|
|
69
|
+
* client it was issued for.
|
|
70
|
+
*/
|
|
71
|
+
readCode(code: string): Promise<Subject | undefined>;
|
|
72
|
+
/**
|
|
73
|
+
* Spends a code, answering whether this call was the one that did. The first
|
|
74
|
+
* call answers true; every later call for the same string answers false,
|
|
75
|
+
* which is what makes an exchange single-use even when two requests race.
|
|
76
|
+
*/
|
|
77
|
+
claimCode(code: string): Promise<boolean>;
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/identity/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,iFAAiF;AACjF,MAAM,WAAW,OAAO;IACtB,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAA;IACX,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;IACnB,OAAO,EAAE,OAAO,CAAA;IAChB,QAAQ,EAAE,OAAO,CAAA;IACjB,qBAAqB,EAAE,OAAO,CAAA;CAC/B;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,MAAM,CAAA;IAChB,kEAAkE;IAClE,qBAAqB,EAAE,MAAM,CAAA;IAC7B,qCAAqC;IACrC,sBAAsB,EAAE,MAAM,CAAA;CAC/B;AAED,MAAM,WAAW,SAAS;IACxB,WAAW,EAAE,MAAM,CAAA;IACnB,YAAY,EAAE,MAAM,CAAA;CACrB;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,aAAa;IAC5B,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,SAAS,CAAC,CAAA;IAC/D,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,SAAS,CAAC,CAAA;IACxD;;;OAGG;IACH,eAAe,CAAC,IAAI,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IACvE,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC,CAAA;IAC7D,+DAA+D;IAC/D,KAAK,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,SAAS,CAAC,CAAA;IACnE,qFAAqF;IACrF,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC,CAAA;IACvD,6EAA6E;IAC7E,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC,CAAA;IACxD,sEAAsE;IACtE,OAAO,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC,CAAA;IACzD,QAAQ,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC1D,UAAU,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC3C,0DAA0D;IAC1D,SAAS,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;IAC5C;;;;;OAKG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC,CAAA;IACpD;;;;OAIG;IACH,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;CAC1C"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The identity the API works over, as a host supplies it.
|
|
3
|
+
*
|
|
4
|
+
* A server holds users in a database, verifies a bcrypt hash and signs JWTs; a
|
|
5
|
+
* browser build holds a seeded fixture, compares a plain-text demo password and
|
|
6
|
+
* mints opaque strings. Those differences are the host's, so they live behind
|
|
7
|
+
* this port - the statuses, the messages and the order of the checks are the
|
|
8
|
+
* service's, and both hosts run the same corpus over them.
|
|
9
|
+
*/
|
|
10
|
+
export {};
|
package/build/esm/index.d.ts
CHANGED
|
@@ -24,7 +24,14 @@ export { OVERLAY, TAG_CAPABILITIES } from './routes/overlay.js';
|
|
|
24
24
|
export type { RouteOverlay } from './routes/overlay.js';
|
|
25
25
|
export { dispatch, HANDLED_ROUTES } from './host/dispatch.js';
|
|
26
26
|
export type { ApiRequest, ApiResponse, Services } from './host/dispatch.js';
|
|
27
|
+
export { createIdentityService } from './identity/service.js';
|
|
28
|
+
export type { IdentityService, LoginResult, SessionResult } from './identity/service.js';
|
|
29
|
+
export { MemoryIdentityStore } from './identity/memory-store.js';
|
|
30
|
+
export { DEFAULT_IDENTITY, IDENTITY_FORMAT, parseIdentity } from './identity/fixture.js';
|
|
31
|
+
export type { IdentityFixture, IdentityFixtureClient, IdentityFixtureCode, IdentityFixtureGroup, IdentityFixturePermission, IdentityFixtureToken, IdentityFixtureUser } from './identity/fixture.js';
|
|
32
|
+
export type { IdentityClient, IdentityStore, IdentityUser, Subject, TokenPair } from './identity/types.js';
|
|
27
33
|
export { DRIVE_CORPUS } from './corpus/drive.js';
|
|
34
|
+
export { IDENTITY_CORPUS } from './corpus/identity.js';
|
|
28
35
|
export type { CorpusCase } from './corpus/drive.js';
|
|
29
36
|
export { runCase, servicesFor } from './corpus/runner.js';
|
|
30
37
|
export type { CaseResult } from './corpus/runner.js';
|
package/build/esm/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,cAAc,YAAY,CAAA;AAC1B,OAAO,EACL,kBAAkB,EAClB,mBAAmB,EACnB,OAAO,EACP,QAAQ,EACR,MAAM,EACP,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;AACvD,YAAY,EACV,YAAY,EACZ,WAAW,EACX,YAAY,EACZ,SAAS,EACT,SAAS,EACV,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAA;AAC1D,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAA;AAChE,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AACnD,OAAO,EAAE,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAA;AAC/D,YAAY,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AACvD,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AAC7D,YAAY,EAAE,UAAU,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAA;AAC3E,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAChD,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AACnD,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AACzD,YAAY,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,cAAc,YAAY,CAAA;AAC1B,OAAO,EACL,kBAAkB,EAClB,mBAAmB,EACnB,OAAO,EACP,QAAQ,EACR,MAAM,EACP,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;AACvD,YAAY,EACV,YAAY,EACZ,WAAW,EACX,YAAY,EACZ,SAAS,EACT,SAAS,EACV,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAA;AAC1D,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAA;AAChE,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AACnD,OAAO,EAAE,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAA;AAC/D,YAAY,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AACvD,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AAC7D,YAAY,EAAE,UAAU,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAA;AAC3E,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAA;AAC7D,YAAY,EACV,eAAe,EACf,WAAW,EACX,aAAa,EACd,MAAM,uBAAuB,CAAA;AAC9B,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAA;AAChE,OAAO,EACL,gBAAgB,EAChB,eAAe,EACf,aAAa,EACd,MAAM,uBAAuB,CAAA;AAC9B,YAAY,EACV,eAAe,EACf,qBAAqB,EACrB,mBAAmB,EACnB,oBAAoB,EACpB,yBAAyB,EACzB,oBAAoB,EACpB,mBAAmB,EACpB,MAAM,uBAAuB,CAAA;AAC9B,YAAY,EACV,cAAc,EACd,aAAa,EACb,YAAY,EACZ,OAAO,EACP,SAAS,EACV,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAChD,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AACtD,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AACnD,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AACzD,YAAY,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA"}
|
package/build/esm/index.js
CHANGED
|
@@ -20,5 +20,9 @@ export { MemoryDriveStore } from './drive/memory-store.js';
|
|
|
20
20
|
export { ROUTES, routeById, findRoute } from './routes/index.js';
|
|
21
21
|
export { OVERLAY, TAG_CAPABILITIES } from './routes/overlay.js';
|
|
22
22
|
export { dispatch, HANDLED_ROUTES } from './host/dispatch.js';
|
|
23
|
+
export { createIdentityService } from './identity/service.js';
|
|
24
|
+
export { MemoryIdentityStore } from './identity/memory-store.js';
|
|
25
|
+
export { DEFAULT_IDENTITY, IDENTITY_FORMAT, parseIdentity } from './identity/fixture.js';
|
|
23
26
|
export { DRIVE_CORPUS } from './corpus/drive.js';
|
|
27
|
+
export { IDENTITY_CORPUS } from './corpus/identity.js';
|
|
24
28
|
export { runCase, servicesFor } from './corpus/runner.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generated.d.ts","sourceRoot":"","sources":["../../../src/routes/generated.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAEjD,eAAO,MAAM,gBAAgB,EAAE,cAAc,
|
|
1
|
+
{"version":3,"file":"generated.d.ts","sourceRoot":"","sources":["../../../src/routes/generated.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAEjD,eAAO,MAAM,gBAAgB,EAAE,cAAc,EA6oD5C,CAAA"}
|