@effect-auth/core 0.1.0-alpha.18 → 0.1.0-alpha.19
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 +4 -5
- package/dist/DevEmail.d.ts +44 -0
- package/dist/DevEmail.d.ts.map +1 -0
- package/dist/DevEmail.js +115 -0
- package/dist/DevEmail.js.map +1 -0
- package/dist/DevelopmentSeed.d.ts +47 -0
- package/dist/DevelopmentSeed.d.ts.map +1 -0
- package/dist/DevelopmentSeed.js +337 -0
- package/dist/DevelopmentSeed.js.map +1 -0
- package/dist/DrizzleBunSqliteStorage.d.ts +1 -1
- package/dist/DrizzleBunSqliteStorage.d.ts.map +1 -1
- package/dist/DrizzleD1SqliteStorage.d.ts +1 -1
- package/dist/DrizzleD1SqliteStorage.d.ts.map +1 -1
- package/dist/DrizzleEffectSqliteStorage.d.ts +1 -1
- package/dist/DrizzleEffectSqliteStorage.d.ts.map +1 -1
- package/dist/DrizzleNodeSqliteStorage.d.ts +1 -1
- package/dist/DrizzleNodeSqliteStorage.d.ts.map +1 -1
- package/dist/DrizzlePostgresStorage.d.ts +26 -0
- package/dist/DrizzlePostgresStorage.d.ts.map +1 -0
- package/dist/DrizzlePostgresStorage.js +14 -0
- package/dist/DrizzlePostgresStorage.js.map +1 -0
- package/dist/EffectQbPostgresStorage.d.ts +8 -1
- package/dist/EffectQbPostgresStorage.d.ts.map +1 -1
- package/dist/EffectQbPostgresStorage.js +359 -2
- package/dist/EffectQbPostgresStorage.js.map +1 -1
- package/dist/EffectQbSqliteStorage.d.ts +42 -2
- package/dist/EffectQbSqliteStorage.d.ts.map +1 -1
- package/dist/EffectQbSqliteStorage.js +337 -1
- package/dist/EffectQbSqliteStorage.js.map +1 -1
- package/dist/HttpApi/Api.d.ts +151 -98
- package/dist/HttpApi/Api.d.ts.map +1 -1
- package/dist/HttpApi/Api.js +122 -72
- package/dist/HttpApi/Api.js.map +1 -1
- package/dist/HttpApi/ClientApi.d.ts +136 -136
- package/dist/HttpApi/Endpoints.d.ts +33 -1
- package/dist/HttpApi/Endpoints.d.ts.map +1 -1
- package/dist/HttpApi/Endpoints.js +31 -2
- package/dist/HttpApi/Endpoints.js.map +1 -1
- package/dist/HttpApi/Errors.d.ts +11 -3
- package/dist/HttpApi/Errors.d.ts.map +1 -1
- package/dist/HttpApi/Errors.js +16 -0
- package/dist/HttpApi/Errors.js.map +1 -1
- package/dist/HttpApi/Middleware.d.ts +12 -0
- package/dist/HttpApi/Middleware.d.ts.map +1 -1
- package/dist/HttpApi/Middleware.js +8 -0
- package/dist/HttpApi/Middleware.js.map +1 -1
- package/dist/HttpApi/OAuthDeviceAuthorization.d.ts +106 -0
- package/dist/HttpApi/OAuthDeviceAuthorization.d.ts.map +1 -0
- package/dist/HttpApi/OAuthDeviceAuthorization.js +415 -0
- package/dist/HttpApi/OAuthDeviceAuthorization.js.map +1 -0
- package/dist/HttpApi/OAuthToken.d.ts +1 -1
- package/dist/HttpApi/OAuthToken.d.ts.map +1 -1
- package/dist/HttpApi/OAuthToken.js +1 -1
- package/dist/HttpApi/OAuthToken.js.map +1 -1
- package/dist/HttpApi/Operations/OAuthDeviceAuthorization.d.ts +23 -0
- package/dist/HttpApi/Operations/OAuthDeviceAuthorization.d.ts.map +1 -0
- package/dist/HttpApi/Operations/OAuthDeviceAuthorization.js +7 -0
- package/dist/HttpApi/Operations/OAuthDeviceAuthorization.js.map +1 -0
- package/dist/HttpApi/Operations/OAuthToken.d.ts +10 -0
- package/dist/HttpApi/Operations/OAuthToken.d.ts.map +1 -1
- package/dist/HttpApi/Operations/OAuthToken.js +4 -0
- package/dist/HttpApi/Operations/OAuthToken.js.map +1 -1
- package/dist/HttpApi/Operations/index.d.ts +1 -0
- package/dist/HttpApi/Operations/index.d.ts.map +1 -1
- package/dist/HttpApi/Operations/index.js +1 -0
- package/dist/HttpApi/Operations/index.js.map +1 -1
- package/dist/HttpApi/PasswordContract.d.ts +9 -9
- package/dist/HttpApi/Schemas.d.ts +38 -0
- package/dist/HttpApi/Schemas.d.ts.map +1 -1
- package/dist/HttpApi/Schemas.js +36 -1
- package/dist/HttpApi/Schemas.js.map +1 -1
- package/dist/HttpApi/index.d.ts +3 -1
- package/dist/HttpApi/index.d.ts.map +1 -1
- package/dist/HttpApi/index.js +3 -1
- package/dist/HttpApi/index.js.map +1 -1
- package/dist/OAuth.d.ts +46 -1
- package/dist/OAuth.d.ts.map +1 -1
- package/dist/OAuth.js +118 -4
- package/dist/OAuth.js.map +1 -1
- package/dist/OAuthDeviceAuthorization.d.ts +378 -0
- package/dist/OAuthDeviceAuthorization.d.ts.map +1 -0
- package/dist/OAuthDeviceAuthorization.js +860 -0
- package/dist/OAuthDeviceAuthorization.js.map +1 -0
- package/dist/OAuthDeviceClient.d.ts +76 -0
- package/dist/OAuthDeviceClient.d.ts.map +1 -0
- package/dist/OAuthDeviceClient.js +362 -0
- package/dist/OAuthDeviceClient.js.map +1 -0
- package/dist/PostgresStorageMigrations.d.ts +2 -1
- package/dist/PostgresStorageMigrations.d.ts.map +1 -1
- package/dist/PostgresStorageMigrations.js +54 -0
- package/dist/PostgresStorageMigrations.js.map +1 -1
- package/dist/Retention.d.ts +6 -1
- package/dist/Retention.d.ts.map +1 -1
- package/dist/Retention.js +49 -12
- package/dist/Retention.js.map +1 -1
- package/dist/SqliteStorageSchemaData.d.ts +2 -0
- package/dist/SqliteStorageSchemaData.d.ts.map +1 -0
- package/dist/SqliteStorageSchemaData.js +508 -0
- package/dist/SqliteStorageSchemaData.js.map +1 -0
- package/dist/StorageMigrations.d.ts +2 -1
- package/dist/StorageMigrations.d.ts.map +1 -1
- package/dist/StorageMigrations.js +32 -0
- package/dist/StorageMigrations.js.map +1 -1
- package/dist/StorageSchemaGenerator.d.ts +70 -12
- package/dist/StorageSchemaGenerator.d.ts.map +1 -1
- package/dist/StorageSchemaGenerator.js +478 -82
- package/dist/StorageSchemaGenerator.js.map +1 -1
- package/migrations/0029_auth_oauth_device_authorization.sql +28 -0
- package/migrations/postgres/0010_auth_oauth_device_authorization.sql +50 -0
- package/package.json +24 -16
- package/dist/DrizzleDurableObjectSqliteStorage.d.ts +0 -19
- package/dist/DrizzleDurableObjectSqliteStorage.d.ts.map +0 -1
- package/dist/DrizzleDurableObjectSqliteStorage.js +0 -14
- package/dist/DrizzleDurableObjectSqliteStorage.js.map +0 -1
- package/dist/DrizzleWasmSqliteStorage.d.ts +0 -23
- package/dist/DrizzleWasmSqliteStorage.d.ts.map +0 -1
- package/dist/DrizzleWasmSqliteStorage.js +0 -15
- package/dist/DrizzleWasmSqliteStorage.js.map +0 -1
|
@@ -0,0 +1,860 @@
|
|
|
1
|
+
import { Brand, Context, Data, Duration, Effect, Layer, Option, Redacted, Ref, } from "effect";
|
|
2
|
+
import { Crypto } from "./Crypto.js";
|
|
3
|
+
import { OAuthClientId, UnixMillis } from "./Identifiers.js";
|
|
4
|
+
import { currentUnixMillis } from "./Internal.js";
|
|
5
|
+
import { defaultOAuthProviderModeAccessTokenBytes, defaultOAuthProviderModeAccessTokenTtl, defaultOAuthProviderModeRefreshTokenBytes, defaultOAuthProviderModeRefreshTokenTtl, OAuthAccessToken, OAuthClientRegistry, OAuthRefreshToken, } from "./OAuth.js";
|
|
6
|
+
export const oauthDeviceCodeGrantType = "urn:ietf:params:oauth:grant-type:device_code";
|
|
7
|
+
export const OAuthDeviceCode = Brand.nominal();
|
|
8
|
+
export const OAuthDeviceUserCode = Brand.nominal();
|
|
9
|
+
export const OAuthDeviceAuthorizationId = Brand.nominal();
|
|
10
|
+
export const oauthDeviceUserCodeAlphabet = "BCDFGHJKLMNPQRSTVWXZ";
|
|
11
|
+
export const defaultOAuthDeviceCodeBytes = 32;
|
|
12
|
+
export const defaultOAuthDeviceAuthorizationIdBytes = 16;
|
|
13
|
+
export const defaultOAuthDeviceAuthorizationTtl = Duration.minutes(10);
|
|
14
|
+
export const defaultOAuthDeviceAuthorizationPollInterval = Duration.seconds(5);
|
|
15
|
+
export const defaultOAuthDeviceAuthorizationCollisionAttempts = 8;
|
|
16
|
+
export const oauthDeviceAuthorizationSlowDownSeconds = 5;
|
|
17
|
+
export class OAuthDeviceAuthorizationStoreError extends Data.TaggedError("OAuthDeviceAuthorizationStoreError") {
|
|
18
|
+
}
|
|
19
|
+
export class OAuthDeviceAuthorizationStore extends Context.Service()("auth/OAuthDeviceAuthorizationStore") {
|
|
20
|
+
}
|
|
21
|
+
export class OAuthDeviceAuthorizationError extends Data.TaggedError("OAuthDeviceAuthorizationError") {
|
|
22
|
+
}
|
|
23
|
+
export class OAuthDeviceAuthorization extends Context.Service()("auth/OAuthDeviceAuthorization", {
|
|
24
|
+
make: (options) => buildOAuthDeviceAuthorization(options),
|
|
25
|
+
}) {
|
|
26
|
+
}
|
|
27
|
+
export class OAuthDeviceCodeTokenIssuerError extends Data.TaggedError("OAuthDeviceCodeTokenIssuerError") {
|
|
28
|
+
}
|
|
29
|
+
export class OAuthDeviceCodeTokenIssuer extends Context.Service()("auth/OAuthDeviceCodeTokenIssuer") {
|
|
30
|
+
}
|
|
31
|
+
export class OAuthDeviceCodeTokenCommitStoreError extends Data.TaggedError("OAuthDeviceCodeTokenCommitStoreError") {
|
|
32
|
+
}
|
|
33
|
+
export class OAuthDeviceCodeTokenCommitStore extends Context.Service()("auth/OAuthDeviceCodeTokenCommitStore") {
|
|
34
|
+
}
|
|
35
|
+
export class OAuthDeviceCodeTokenGrantError extends Data.TaggedError("OAuthDeviceCodeTokenGrantError") {
|
|
36
|
+
}
|
|
37
|
+
export class OAuthDeviceCodeTokenGrant extends Context.Service()("auth/OAuthDeviceCodeTokenGrant", {
|
|
38
|
+
make: () => buildOAuthDeviceCodeTokenGrant(),
|
|
39
|
+
}) {
|
|
40
|
+
}
|
|
41
|
+
const cloneMetadata = (metadata) => metadata === undefined ? undefined : structuredClone(metadata);
|
|
42
|
+
const cloneClient = (client) => ({
|
|
43
|
+
...client,
|
|
44
|
+
redirectUris: [...client.redirectUris],
|
|
45
|
+
allowedGrantTypes: [...client.allowedGrantTypes],
|
|
46
|
+
allowedResponseTypes: [...client.allowedResponseTypes],
|
|
47
|
+
...(client.allowedScopes === undefined
|
|
48
|
+
? {}
|
|
49
|
+
: { allowedScopes: [...client.allowedScopes] }),
|
|
50
|
+
...(client.metadata === undefined
|
|
51
|
+
? {}
|
|
52
|
+
: { metadata: cloneMetadata(client.metadata) }),
|
|
53
|
+
});
|
|
54
|
+
const cloneRecord = (record) => ({
|
|
55
|
+
...record,
|
|
56
|
+
requestedScopes: [...record.requestedScopes],
|
|
57
|
+
...(record.status === "approved"
|
|
58
|
+
? { grantedScopes: [...record.grantedScopes] }
|
|
59
|
+
: {}),
|
|
60
|
+
...(record.metadata === undefined
|
|
61
|
+
? {}
|
|
62
|
+
: { metadata: cloneMetadata(record.metadata) }),
|
|
63
|
+
});
|
|
64
|
+
const clonePendingRecord = (record) => cloneRecord(record);
|
|
65
|
+
const cloneApprovedRecord = (record) => cloneRecord(record);
|
|
66
|
+
const cloneUnconsumedApprovedRecord = (record) => cloneRecord(record);
|
|
67
|
+
const cloneDeniedRecord = (record) => cloneRecord(record);
|
|
68
|
+
const cloneConsumedRecord = (record) => cloneRecord(record);
|
|
69
|
+
const cloneTokenResponse = (tokens) => ({
|
|
70
|
+
accessToken: tokens.accessToken,
|
|
71
|
+
tokenType: tokens.tokenType,
|
|
72
|
+
...(tokens.expiresIn === undefined ? {} : { expiresIn: tokens.expiresIn }),
|
|
73
|
+
...(tokens.refreshToken === undefined
|
|
74
|
+
? {}
|
|
75
|
+
: { refreshToken: tokens.refreshToken }),
|
|
76
|
+
...(tokens.idToken === undefined ? {} : { idToken: tokens.idToken }),
|
|
77
|
+
...(tokens.scopes === undefined ? {} : { scopes: [...tokens.scopes] }),
|
|
78
|
+
...(tokens.extra === undefined ? {} : { extra: cloneMetadata(tokens.extra) }),
|
|
79
|
+
});
|
|
80
|
+
const authorizationDetails = (record) => ({
|
|
81
|
+
id: record.id,
|
|
82
|
+
clientId: record.clientId,
|
|
83
|
+
requestedScopes: [...record.requestedScopes],
|
|
84
|
+
status: record.status,
|
|
85
|
+
issuedAt: record.issuedAt,
|
|
86
|
+
expiresAt: record.expiresAt,
|
|
87
|
+
pollIntervalSeconds: record.pollIntervalSeconds,
|
|
88
|
+
nextPollAt: record.nextPollAt,
|
|
89
|
+
...(record.lastPolledAt === undefined
|
|
90
|
+
? {}
|
|
91
|
+
: { lastPolledAt: record.lastPolledAt }),
|
|
92
|
+
...(record.status === "approved"
|
|
93
|
+
? {
|
|
94
|
+
subject: record.subject,
|
|
95
|
+
grantedScopes: [...record.grantedScopes],
|
|
96
|
+
approvedAt: record.approvedAt,
|
|
97
|
+
...(record.consumedAt === undefined
|
|
98
|
+
? {}
|
|
99
|
+
: { consumedAt: record.consumedAt }),
|
|
100
|
+
}
|
|
101
|
+
: record.status === "denied"
|
|
102
|
+
? { deniedAt: record.deniedAt }
|
|
103
|
+
: {}),
|
|
104
|
+
...(record.metadata === undefined
|
|
105
|
+
? {}
|
|
106
|
+
: { metadata: cloneMetadata(record.metadata) }),
|
|
107
|
+
});
|
|
108
|
+
const tokenIssueAuthorization = (record) => ({
|
|
109
|
+
id: record.id,
|
|
110
|
+
clientId: record.clientId,
|
|
111
|
+
requestedScopes: [...record.requestedScopes],
|
|
112
|
+
issuedAt: record.issuedAt,
|
|
113
|
+
expiresAt: record.expiresAt,
|
|
114
|
+
approvedAt: record.approvedAt,
|
|
115
|
+
consumedAt: record.consumedAt,
|
|
116
|
+
...(record.metadata === undefined
|
|
117
|
+
? {}
|
|
118
|
+
: { metadata: cloneMetadata(record.metadata) }),
|
|
119
|
+
});
|
|
120
|
+
const tokenPrepareAuthorization = (record) => ({
|
|
121
|
+
id: record.id,
|
|
122
|
+
clientId: record.clientId,
|
|
123
|
+
requestedScopes: [...record.requestedScopes],
|
|
124
|
+
issuedAt: record.issuedAt,
|
|
125
|
+
expiresAt: record.expiresAt,
|
|
126
|
+
approvedAt: record.approvedAt,
|
|
127
|
+
...(record.metadata === undefined
|
|
128
|
+
? {}
|
|
129
|
+
: { metadata: cloneMetadata(record.metadata) }),
|
|
130
|
+
});
|
|
131
|
+
const cloneProviderModeTokenRecord = (record) => ({
|
|
132
|
+
...record,
|
|
133
|
+
scopes: [...record.scopes],
|
|
134
|
+
...(record.audience === undefined
|
|
135
|
+
? {}
|
|
136
|
+
: {
|
|
137
|
+
audience: Array.isArray(record.audience)
|
|
138
|
+
? [...record.audience]
|
|
139
|
+
: record.audience,
|
|
140
|
+
}),
|
|
141
|
+
...(record.metadata === undefined
|
|
142
|
+
? {}
|
|
143
|
+
: { metadata: cloneMetadata(record.metadata) }),
|
|
144
|
+
});
|
|
145
|
+
const cloneTokenIssueAuthorization = (authorization) => ({
|
|
146
|
+
...authorization,
|
|
147
|
+
requestedScopes: [...authorization.requestedScopes],
|
|
148
|
+
...(authorization.metadata === undefined
|
|
149
|
+
? {}
|
|
150
|
+
: { metadata: cloneMetadata(authorization.metadata) }),
|
|
151
|
+
});
|
|
152
|
+
const normalizeScopes = (scopes) => {
|
|
153
|
+
const values = typeof scopes === "string" ? scopes.split(/\s+/) : (scopes ?? []);
|
|
154
|
+
const normalized = new Set();
|
|
155
|
+
for (const value of values) {
|
|
156
|
+
const scope = value.trim();
|
|
157
|
+
if (scope !== "") {
|
|
158
|
+
if (!/^[\u0021\u0023-\u005B\u005D-\u007E]+$/.test(scope)) {
|
|
159
|
+
return undefined;
|
|
160
|
+
}
|
|
161
|
+
normalized.add(scope);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return [...normalized];
|
|
165
|
+
};
|
|
166
|
+
const normalizedClientId = (value) => {
|
|
167
|
+
const clientId = value === undefined ? "" : String(value).trim();
|
|
168
|
+
return clientId === "" ? undefined : OAuthClientId(clientId);
|
|
169
|
+
};
|
|
170
|
+
export const normalizeOAuthDeviceUserCode = (value) => {
|
|
171
|
+
const compact = String(value).trim().toUpperCase().replaceAll(/[\s-]/g, "");
|
|
172
|
+
if (compact.length !== 8 ||
|
|
173
|
+
[...compact].some((character) => !oauthDeviceUserCodeAlphabet.includes(character))) {
|
|
174
|
+
return undefined;
|
|
175
|
+
}
|
|
176
|
+
return OAuthDeviceUserCode(`${compact.slice(0, 4)}-${compact.slice(4)}`);
|
|
177
|
+
};
|
|
178
|
+
const authorizationError = (operation, message, cause) => new OAuthDeviceAuthorizationError({
|
|
179
|
+
operation,
|
|
180
|
+
message,
|
|
181
|
+
...(cause === undefined ? {} : { cause }),
|
|
182
|
+
});
|
|
183
|
+
const tokenGrantError = (message, cause) => new OAuthDeviceCodeTokenGrantError({
|
|
184
|
+
operation: "exchange",
|
|
185
|
+
message,
|
|
186
|
+
...(cause === undefined ? {} : { cause }),
|
|
187
|
+
});
|
|
188
|
+
const deviceTokenIssuerError = (message, cause) => new OAuthDeviceCodeTokenIssuerError({
|
|
189
|
+
operation: "prepare",
|
|
190
|
+
message,
|
|
191
|
+
...(cause === undefined ? {} : { cause }),
|
|
192
|
+
});
|
|
193
|
+
const validateByteLength = (value, label) => Number.isInteger(value) && value >= 16 && value <= 128
|
|
194
|
+
? Effect.succeed(value)
|
|
195
|
+
: Effect.fail(authorizationError("start", `${label} byte length must be an integer between 16 and 128`));
|
|
196
|
+
const validateOptions = (options) => Effect.gen(function* () {
|
|
197
|
+
const deviceCodeBytes = yield* validateByteLength(options.deviceCodeBytes ?? defaultOAuthDeviceCodeBytes, "OAuth device code");
|
|
198
|
+
const authorizationIdBytes = yield* validateByteLength(options.authorizationIdBytes ?? defaultOAuthDeviceAuthorizationIdBytes, "OAuth device authorization id");
|
|
199
|
+
const ttlMillis = Duration.toMillis(options.ttl ?? defaultOAuthDeviceAuthorizationTtl);
|
|
200
|
+
const pollIntervalMillis = Duration.toMillis(options.pollInterval ?? defaultOAuthDeviceAuthorizationPollInterval);
|
|
201
|
+
const collisionAttempts = options.collisionAttempts ??
|
|
202
|
+
defaultOAuthDeviceAuthorizationCollisionAttempts;
|
|
203
|
+
if (!Number.isSafeInteger(ttlMillis) ||
|
|
204
|
+
ttlMillis < 1000 ||
|
|
205
|
+
ttlMillis % 1000 !== 0) {
|
|
206
|
+
return yield* authorizationError("start", "OAuth device authorization TTL must be a positive whole number of seconds");
|
|
207
|
+
}
|
|
208
|
+
if (!Number.isFinite(pollIntervalMillis) ||
|
|
209
|
+
pollIntervalMillis < 1000 ||
|
|
210
|
+
pollIntervalMillis % 1000 !== 0) {
|
|
211
|
+
return yield* authorizationError("start", "OAuth device authorization poll interval must be a positive whole number of seconds");
|
|
212
|
+
}
|
|
213
|
+
const secretBytes = options.userCodeSecret instanceof Uint8Array
|
|
214
|
+
? options.userCodeSecret.byteLength
|
|
215
|
+
: new TextEncoder().encode(Redacted.value(options.userCodeSecret))
|
|
216
|
+
.byteLength;
|
|
217
|
+
if (secretBytes < 32) {
|
|
218
|
+
return yield* authorizationError("start", "OAuth device user code secret must contain at least 32 bytes");
|
|
219
|
+
}
|
|
220
|
+
if (!Number.isInteger(collisionAttempts) ||
|
|
221
|
+
collisionAttempts < 1 ||
|
|
222
|
+
collisionAttempts > 100) {
|
|
223
|
+
return yield* authorizationError("start", "OAuth device authorization collision attempts must be an integer between 1 and 100");
|
|
224
|
+
}
|
|
225
|
+
return {
|
|
226
|
+
deviceCodeBytes,
|
|
227
|
+
authorizationIdBytes,
|
|
228
|
+
ttlMillis,
|
|
229
|
+
pollIntervalSeconds: pollIntervalMillis / 1000,
|
|
230
|
+
collisionAttempts,
|
|
231
|
+
};
|
|
232
|
+
});
|
|
233
|
+
const deviceCodeHash = (crypto, deviceCode, error) => crypto.digestSha256({ data: deviceCode }).pipe(Effect.map((hash) => `sha256:${hash}`), Effect.mapError((cause) => error("Failed to hash OAuth device code", cause)));
|
|
234
|
+
const userCodeHash = (crypto, secret, userCode, operation) => crypto.hmacSha256({ key: secret, data: userCode }).pipe(Effect.map((hash) => `hmac-sha256:${hash}`), Effect.mapError((cause) => authorizationError(operation, "Failed to hash OAuth device user code", cause)));
|
|
235
|
+
const generateUserCode = (crypto) => Effect.gen(function* () {
|
|
236
|
+
const characters = [];
|
|
237
|
+
for (let batch = 0; characters.length < 8 && batch < 16; batch += 1) {
|
|
238
|
+
const bytes = yield* crypto
|
|
239
|
+
.randomBytes(16)
|
|
240
|
+
.pipe(Effect.mapError((cause) => authorizationError("start", "Failed to generate OAuth device user code", cause)));
|
|
241
|
+
for (const byte of bytes) {
|
|
242
|
+
if (byte < 240) {
|
|
243
|
+
characters.push(oauthDeviceUserCodeAlphabet[byte % 20]);
|
|
244
|
+
if (characters.length === 8) {
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
if (characters.length !== 8) {
|
|
251
|
+
return yield* authorizationError("start", "Failed to generate an unbiased OAuth device user code");
|
|
252
|
+
}
|
|
253
|
+
return OAuthDeviceUserCode(`${characters.slice(0, 4).join("")}-${characters.slice(4).join("")}`);
|
|
254
|
+
});
|
|
255
|
+
const startInvalid = (reason) => ({ valid: false, reason });
|
|
256
|
+
const decisionInvalid = (reason) => ({ valid: false, reason });
|
|
257
|
+
const tokenGrantInvalid = (reason) => ({ valid: false, reason });
|
|
258
|
+
const buildOAuthDeviceAuthorization = (constructionOptions) => Effect.gen(function* () {
|
|
259
|
+
const crypto = yield* Crypto;
|
|
260
|
+
const store = yield* OAuthDeviceAuthorizationStore;
|
|
261
|
+
const clients = yield* OAuthClientRegistry;
|
|
262
|
+
const options = yield* validateOptions(constructionOptions);
|
|
263
|
+
const { userCodeSecret } = constructionOptions;
|
|
264
|
+
return {
|
|
265
|
+
start: Effect.fn("auth.oauth.device_authorization.start")(function* (input) {
|
|
266
|
+
const clientId = normalizedClientId(input.clientId);
|
|
267
|
+
if (clientId === undefined) {
|
|
268
|
+
return startInvalid("invalid_request");
|
|
269
|
+
}
|
|
270
|
+
const clientOption = yield* clients
|
|
271
|
+
.find({ clientId })
|
|
272
|
+
.pipe(Effect.mapError((cause) => authorizationError("start", "Failed to load OAuth client", cause)));
|
|
273
|
+
if (Option.isNone(clientOption) ||
|
|
274
|
+
clientOption.value.status !== "active") {
|
|
275
|
+
return startInvalid("invalid_client");
|
|
276
|
+
}
|
|
277
|
+
const client = clientOption.value;
|
|
278
|
+
if (!client.allowedGrantTypes.includes(oauthDeviceCodeGrantType)) {
|
|
279
|
+
return startInvalid("unauthorized_client");
|
|
280
|
+
}
|
|
281
|
+
const requestedScopes = normalizeScopes(input.scopes);
|
|
282
|
+
if (requestedScopes === undefined) {
|
|
283
|
+
return startInvalid("invalid_scope");
|
|
284
|
+
}
|
|
285
|
+
if (client.allowedScopes !== undefined &&
|
|
286
|
+
requestedScopes.some((scope) => !client.allowedScopes?.includes(scope))) {
|
|
287
|
+
return startInvalid("invalid_scope");
|
|
288
|
+
}
|
|
289
|
+
const issuedAt = yield* currentUnixMillis;
|
|
290
|
+
const expiresAt = UnixMillis(Number(issuedAt) + options.ttlMillis);
|
|
291
|
+
const nextPollAt = UnixMillis(Number(issuedAt) + options.pollIntervalSeconds * 1000);
|
|
292
|
+
for (let attempt = 0; attempt < options.collisionAttempts; attempt += 1) {
|
|
293
|
+
const authorizationId = OAuthDeviceAuthorizationId(yield* crypto
|
|
294
|
+
.randomToken(options.authorizationIdBytes)
|
|
295
|
+
.pipe(Effect.mapError((cause) => authorizationError("start", "Failed to generate OAuth device authorization id", cause))));
|
|
296
|
+
const rawDeviceCode = OAuthDeviceCode(yield* crypto
|
|
297
|
+
.randomToken(options.deviceCodeBytes)
|
|
298
|
+
.pipe(Effect.mapError((cause) => authorizationError("start", "Failed to generate OAuth device code", cause))));
|
|
299
|
+
const rawUserCode = yield* generateUserCode(crypto);
|
|
300
|
+
const hashedDeviceCode = yield* deviceCodeHash(crypto, rawDeviceCode, (message, cause) => authorizationError("start", message, cause));
|
|
301
|
+
const hashedUserCode = yield* userCodeHash(crypto, userCodeSecret, rawUserCode, "start");
|
|
302
|
+
const record = {
|
|
303
|
+
id: authorizationId,
|
|
304
|
+
deviceCodeHash: hashedDeviceCode,
|
|
305
|
+
userCodeHash: hashedUserCode,
|
|
306
|
+
clientId,
|
|
307
|
+
requestedScopes: [...requestedScopes],
|
|
308
|
+
status: "pending",
|
|
309
|
+
issuedAt,
|
|
310
|
+
expiresAt,
|
|
311
|
+
pollIntervalSeconds: options.pollIntervalSeconds,
|
|
312
|
+
nextPollAt,
|
|
313
|
+
...(input.metadata === undefined
|
|
314
|
+
? {}
|
|
315
|
+
: { metadata: cloneMetadata(input.metadata) }),
|
|
316
|
+
};
|
|
317
|
+
const inserted = yield* store
|
|
318
|
+
.insert(record)
|
|
319
|
+
.pipe(Effect.mapError((cause) => authorizationError("start", "Failed to store OAuth device authorization", cause)));
|
|
320
|
+
if (inserted) {
|
|
321
|
+
return {
|
|
322
|
+
valid: true,
|
|
323
|
+
authorization: authorizationDetails(record),
|
|
324
|
+
client: cloneClient(client),
|
|
325
|
+
deviceCode: Redacted.make(rawDeviceCode),
|
|
326
|
+
userCode: rawUserCode,
|
|
327
|
+
expiresIn: options.ttlMillis / 1000,
|
|
328
|
+
interval: options.pollIntervalSeconds,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return yield* authorizationError("start", "OAuth device authorization code collision limit exceeded");
|
|
333
|
+
}),
|
|
334
|
+
inspect: Effect.fn("auth.oauth.device_authorization.inspect")(function* (input) {
|
|
335
|
+
const normalized = normalizeOAuthDeviceUserCode(input.userCode);
|
|
336
|
+
if (normalized === undefined) {
|
|
337
|
+
return { valid: false, reason: "invalid_user_code" };
|
|
338
|
+
}
|
|
339
|
+
const hash = yield* userCodeHash(crypto, userCodeSecret, normalized, "inspect");
|
|
340
|
+
const recordOption = yield* store
|
|
341
|
+
.findByUserCodeHash({ userCodeHash: hash })
|
|
342
|
+
.pipe(Effect.mapError((cause) => authorizationError("inspect", "Failed to load OAuth device authorization", cause)));
|
|
343
|
+
if (Option.isNone(recordOption)) {
|
|
344
|
+
return { valid: false, reason: "invalid_user_code" };
|
|
345
|
+
}
|
|
346
|
+
const now = yield* currentUnixMillis;
|
|
347
|
+
if (Number(now) >= Number(recordOption.value.expiresAt)) {
|
|
348
|
+
return { valid: false, reason: "expired_token" };
|
|
349
|
+
}
|
|
350
|
+
return {
|
|
351
|
+
valid: true,
|
|
352
|
+
authorization: authorizationDetails(recordOption.value),
|
|
353
|
+
};
|
|
354
|
+
}),
|
|
355
|
+
approve: Effect.fn("auth.oauth.device_authorization.approve")(function* (input) {
|
|
356
|
+
const subject = input.subject.trim();
|
|
357
|
+
if (subject === "") {
|
|
358
|
+
return decisionInvalid("invalid_request");
|
|
359
|
+
}
|
|
360
|
+
const currentOption = yield* store
|
|
361
|
+
.findById(input.authorizationId)
|
|
362
|
+
.pipe(Effect.mapError((cause) => authorizationError("approve", "Failed to load OAuth device authorization", cause)));
|
|
363
|
+
if (Option.isNone(currentOption)) {
|
|
364
|
+
return decisionInvalid("invalid_request");
|
|
365
|
+
}
|
|
366
|
+
const current = currentOption.value;
|
|
367
|
+
const grantedScopes = input.scopes === undefined
|
|
368
|
+
? [...current.requestedScopes]
|
|
369
|
+
: normalizeScopes(input.scopes);
|
|
370
|
+
if (grantedScopes === undefined) {
|
|
371
|
+
return decisionInvalid("invalid_scope");
|
|
372
|
+
}
|
|
373
|
+
if (grantedScopes.some((scope) => !current.requestedScopes.includes(scope))) {
|
|
374
|
+
return decisionInvalid("invalid_scope");
|
|
375
|
+
}
|
|
376
|
+
const now = yield* currentUnixMillis;
|
|
377
|
+
const decided = yield* store
|
|
378
|
+
.decide({
|
|
379
|
+
id: input.authorizationId,
|
|
380
|
+
decision: "approve",
|
|
381
|
+
subject,
|
|
382
|
+
grantedScopes: [...grantedScopes],
|
|
383
|
+
decidedAt: now,
|
|
384
|
+
metadata: current.metadata === undefined && input.metadata === undefined
|
|
385
|
+
? undefined
|
|
386
|
+
: {
|
|
387
|
+
...cloneMetadata(current.metadata),
|
|
388
|
+
...cloneMetadata(input.metadata),
|
|
389
|
+
},
|
|
390
|
+
})
|
|
391
|
+
.pipe(Effect.mapError((cause) => authorizationError("approve", "Failed to approve OAuth device authorization", cause)));
|
|
392
|
+
switch (decided.status) {
|
|
393
|
+
case "approved": {
|
|
394
|
+
return {
|
|
395
|
+
valid: true,
|
|
396
|
+
authorization: authorizationDetails(decided.record),
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
case "expired": {
|
|
400
|
+
return decisionInvalid("expired_token");
|
|
401
|
+
}
|
|
402
|
+
case "already_decided": {
|
|
403
|
+
return decisionInvalid("already_decided");
|
|
404
|
+
}
|
|
405
|
+
case "missing": {
|
|
406
|
+
return decisionInvalid("invalid_request");
|
|
407
|
+
}
|
|
408
|
+
case "denied": {
|
|
409
|
+
return decisionInvalid("already_decided");
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}),
|
|
413
|
+
deny: Effect.fn("auth.oauth.device_authorization.deny")(function* (input) {
|
|
414
|
+
const now = yield* currentUnixMillis;
|
|
415
|
+
const decided = yield* store
|
|
416
|
+
.decide({
|
|
417
|
+
id: input.authorizationId,
|
|
418
|
+
decision: "deny",
|
|
419
|
+
decidedAt: now,
|
|
420
|
+
...(input.metadata === undefined
|
|
421
|
+
? {}
|
|
422
|
+
: { metadata: cloneMetadata(input.metadata) }),
|
|
423
|
+
})
|
|
424
|
+
.pipe(Effect.mapError((cause) => authorizationError("deny", "Failed to deny OAuth device authorization", cause)));
|
|
425
|
+
switch (decided.status) {
|
|
426
|
+
case "denied": {
|
|
427
|
+
return {
|
|
428
|
+
valid: true,
|
|
429
|
+
authorization: authorizationDetails(decided.record),
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
case "expired": {
|
|
433
|
+
return decisionInvalid("expired_token");
|
|
434
|
+
}
|
|
435
|
+
case "already_decided": {
|
|
436
|
+
return decisionInvalid("already_decided");
|
|
437
|
+
}
|
|
438
|
+
case "missing": {
|
|
439
|
+
return decisionInvalid("invalid_request");
|
|
440
|
+
}
|
|
441
|
+
case "approved": {
|
|
442
|
+
return decisionInvalid("already_decided");
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}),
|
|
446
|
+
};
|
|
447
|
+
});
|
|
448
|
+
const buildOAuthDeviceCodeTokenGrant = () => Effect.gen(function* () {
|
|
449
|
+
const crypto = yield* Crypto;
|
|
450
|
+
const store = yield* OAuthDeviceAuthorizationStore;
|
|
451
|
+
const clients = yield* OAuthClientRegistry;
|
|
452
|
+
const issuer = yield* OAuthDeviceCodeTokenIssuer;
|
|
453
|
+
const commitStore = yield* OAuthDeviceCodeTokenCommitStore;
|
|
454
|
+
return {
|
|
455
|
+
exchange: Effect.fn("auth.oauth.device_code_token.exchange")(function* (input) {
|
|
456
|
+
if (input.grantType !== oauthDeviceCodeGrantType) {
|
|
457
|
+
return tokenGrantInvalid("unsupported_grant_type");
|
|
458
|
+
}
|
|
459
|
+
const clientId = normalizedClientId(input.clientId);
|
|
460
|
+
const rawDeviceCode = input.deviceCode === undefined
|
|
461
|
+
? ""
|
|
462
|
+
: String(Redacted.value(input.deviceCode)).trim();
|
|
463
|
+
if (clientId === undefined || rawDeviceCode === "") {
|
|
464
|
+
return tokenGrantInvalid("invalid_request");
|
|
465
|
+
}
|
|
466
|
+
const clientOption = yield* clients
|
|
467
|
+
.find({ clientId })
|
|
468
|
+
.pipe(Effect.mapError((cause) => tokenGrantError("Failed to load OAuth client", cause)));
|
|
469
|
+
if (Option.isNone(clientOption) ||
|
|
470
|
+
clientOption.value.status !== "active") {
|
|
471
|
+
return tokenGrantInvalid("invalid_client");
|
|
472
|
+
}
|
|
473
|
+
const client = clientOption.value;
|
|
474
|
+
if (!client.allowedGrantTypes.includes(oauthDeviceCodeGrantType)) {
|
|
475
|
+
return tokenGrantInvalid("unauthorized_client");
|
|
476
|
+
}
|
|
477
|
+
if (client.type === "confidential" &&
|
|
478
|
+
input.clientAuthenticated !== true) {
|
|
479
|
+
return tokenGrantInvalid("invalid_client");
|
|
480
|
+
}
|
|
481
|
+
const hash = yield* deviceCodeHash(crypto, rawDeviceCode, tokenGrantError);
|
|
482
|
+
const now = yield* currentUnixMillis;
|
|
483
|
+
const polled = yield* store
|
|
484
|
+
.poll({ deviceCodeHash: hash, clientId, now })
|
|
485
|
+
.pipe(Effect.mapError((cause) => tokenGrantError("Failed to poll OAuth device authorization", cause)));
|
|
486
|
+
switch (polled.status) {
|
|
487
|
+
case "missing":
|
|
488
|
+
case "already_consumed": {
|
|
489
|
+
return tokenGrantInvalid("invalid_grant");
|
|
490
|
+
}
|
|
491
|
+
case "expired": {
|
|
492
|
+
return tokenGrantInvalid("expired_token");
|
|
493
|
+
}
|
|
494
|
+
case "denied": {
|
|
495
|
+
return tokenGrantInvalid("access_denied");
|
|
496
|
+
}
|
|
497
|
+
case "pending": {
|
|
498
|
+
return tokenGrantInvalid("authorization_pending");
|
|
499
|
+
}
|
|
500
|
+
case "slow_down": {
|
|
501
|
+
return tokenGrantInvalid("slow_down");
|
|
502
|
+
}
|
|
503
|
+
case "approved": {
|
|
504
|
+
const preparation = yield* issuer
|
|
505
|
+
.prepare({
|
|
506
|
+
client: cloneClient(client),
|
|
507
|
+
authorization: tokenPrepareAuthorization(polled.record),
|
|
508
|
+
subject: polled.record.subject,
|
|
509
|
+
scopes: [...polled.record.grantedScopes],
|
|
510
|
+
now,
|
|
511
|
+
})
|
|
512
|
+
.pipe(Effect.mapError((cause) => tokenGrantError("Failed to prepare OAuth device tokens", cause)));
|
|
513
|
+
const committed = yield* commitStore
|
|
514
|
+
.commit({
|
|
515
|
+
authorization: polled.record,
|
|
516
|
+
consumedAt: now,
|
|
517
|
+
tokenRecords: preparation.tokenRecords.map(cloneProviderModeTokenRecord),
|
|
518
|
+
})
|
|
519
|
+
.pipe(Effect.mapError((cause) => tokenGrantError("Failed to commit OAuth device token exchange", cause)));
|
|
520
|
+
if (committed.status === "missing") {
|
|
521
|
+
return tokenGrantInvalid("invalid_grant");
|
|
522
|
+
}
|
|
523
|
+
if (committed.status === "already_consumed") {
|
|
524
|
+
return tokenGrantInvalid("invalid_grant");
|
|
525
|
+
}
|
|
526
|
+
if (committed.status === "expired") {
|
|
527
|
+
return tokenGrantInvalid("expired_token");
|
|
528
|
+
}
|
|
529
|
+
const authorization = tokenIssueAuthorization(committed.record);
|
|
530
|
+
const scopes = [...committed.record.grantedScopes];
|
|
531
|
+
return {
|
|
532
|
+
valid: true,
|
|
533
|
+
client: cloneClient(client),
|
|
534
|
+
authorization: cloneTokenIssueAuthorization(authorization),
|
|
535
|
+
subject: committed.record.subject,
|
|
536
|
+
scopes: [...scopes],
|
|
537
|
+
tokens: cloneTokenResponse(preparation.tokens),
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}),
|
|
542
|
+
};
|
|
543
|
+
});
|
|
544
|
+
const validateOpaqueTokenBytes = (value, label) => Number.isInteger(value) && value >= 16 && value <= 128
|
|
545
|
+
? Effect.succeed(value)
|
|
546
|
+
: Effect.fail(deviceTokenIssuerError(`${label} byte length must be an integer between 16 and 128`));
|
|
547
|
+
const validateOpaqueTokenTtl = (value, label) => {
|
|
548
|
+
const millis = Duration.toMillis(value);
|
|
549
|
+
return Number.isSafeInteger(millis) && millis >= 1000 && millis % 1000 === 0
|
|
550
|
+
? Effect.succeed(millis)
|
|
551
|
+
: Effect.fail(deviceTokenIssuerError(`${label} TTL must be a positive whole number of seconds`));
|
|
552
|
+
};
|
|
553
|
+
const generateOpaqueToken = (crypto, byteLength, brand, label) => crypto.randomToken(byteLength).pipe(Effect.map((value) => Redacted.make(brand(value))), Effect.mapError((cause) => deviceTokenIssuerError(`Failed to generate OAuth ${label} token`, cause)));
|
|
554
|
+
const hashOpaqueToken = (crypto, token) => crypto.digestSha256({ data: Redacted.value(token) }).pipe(Effect.map((hash) => `sha256:${hash}`), Effect.mapError((cause) => deviceTokenIssuerError("Failed to hash OAuth provider-mode token", cause)));
|
|
555
|
+
export var OAuthDeviceProviderModeOpaqueTokenIssuer;
|
|
556
|
+
(function (OAuthDeviceProviderModeOpaqueTokenIssuer) {
|
|
557
|
+
OAuthDeviceProviderModeOpaqueTokenIssuer.make = (options = {}) => Effect.gen(function* () {
|
|
558
|
+
const crypto = yield* Crypto;
|
|
559
|
+
const accessTokenBytes = yield* validateOpaqueTokenBytes(options.accessTokenBytes ?? defaultOAuthProviderModeAccessTokenBytes, "OAuth access token");
|
|
560
|
+
const refreshTokenBytes = yield* validateOpaqueTokenBytes(options.refreshTokenBytes ?? defaultOAuthProviderModeRefreshTokenBytes, "OAuth refresh token");
|
|
561
|
+
const accessTokenTtlMillis = yield* validateOpaqueTokenTtl(options.accessTokenTtl ?? defaultOAuthProviderModeAccessTokenTtl, "OAuth access token");
|
|
562
|
+
const refreshTokenTtlMillis = yield* validateOpaqueTokenTtl(options.refreshTokenTtl ?? defaultOAuthProviderModeRefreshTokenTtl, "OAuth refresh token");
|
|
563
|
+
const configuredIssuer = options.issuer;
|
|
564
|
+
const configuredMetadata = cloneMetadata(options.metadata);
|
|
565
|
+
const configuredAudience = Array.isArray(options.audience)
|
|
566
|
+
? [...options.audience]
|
|
567
|
+
: options.audience;
|
|
568
|
+
return OAuthDeviceCodeTokenIssuer.of({
|
|
569
|
+
prepare: Effect.fn("auth.oauth.device_code.provider_mode_opaque.prepare")(function* (input) {
|
|
570
|
+
const issueRefreshToken = input.client.allowedGrantTypes.includes("refresh_token");
|
|
571
|
+
const refreshTokenExpiresAt = UnixMillis(Number(input.now) + refreshTokenTtlMillis);
|
|
572
|
+
const accessTokenExpiresAt = UnixMillis(Math.min(Number(input.now) + accessTokenTtlMillis, issueRefreshToken
|
|
573
|
+
? Number(refreshTokenExpiresAt)
|
|
574
|
+
: Number.POSITIVE_INFINITY));
|
|
575
|
+
const metadata = {
|
|
576
|
+
...cloneMetadata(configuredMetadata),
|
|
577
|
+
grantType: oauthDeviceCodeGrantType,
|
|
578
|
+
authorizationId: input.authorization.id,
|
|
579
|
+
};
|
|
580
|
+
const baseRecord = {
|
|
581
|
+
clientId: input.client.id,
|
|
582
|
+
subject: input.subject,
|
|
583
|
+
scopes: [...input.scopes],
|
|
584
|
+
issuedAt: input.now,
|
|
585
|
+
...(configuredIssuer === undefined
|
|
586
|
+
? {}
|
|
587
|
+
: { issuer: configuredIssuer }),
|
|
588
|
+
...(configuredAudience === undefined
|
|
589
|
+
? {}
|
|
590
|
+
: {
|
|
591
|
+
audience: Array.isArray(configuredAudience)
|
|
592
|
+
? [...configuredAudience]
|
|
593
|
+
: configuredAudience,
|
|
594
|
+
}),
|
|
595
|
+
metadata,
|
|
596
|
+
};
|
|
597
|
+
const accessToken = yield* generateOpaqueToken(crypto, accessTokenBytes, OAuthAccessToken, "access");
|
|
598
|
+
const accessTokenHash = yield* hashOpaqueToken(crypto, accessToken);
|
|
599
|
+
const accessTokenRecord = {
|
|
600
|
+
...baseRecord,
|
|
601
|
+
tokenHash: accessTokenHash,
|
|
602
|
+
tokenType: "access_token",
|
|
603
|
+
expiresAt: accessTokenExpiresAt,
|
|
604
|
+
};
|
|
605
|
+
if (!issueRefreshToken) {
|
|
606
|
+
return {
|
|
607
|
+
tokens: {
|
|
608
|
+
accessToken,
|
|
609
|
+
tokenType: "Bearer",
|
|
610
|
+
expiresIn: Math.floor((Number(accessTokenExpiresAt) - Number(input.now)) / 1000),
|
|
611
|
+
...(input.scopes.length === 0
|
|
612
|
+
? {}
|
|
613
|
+
: { scopes: [...input.scopes] }),
|
|
614
|
+
},
|
|
615
|
+
tokenRecords: [accessTokenRecord],
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
const refreshToken = yield* generateOpaqueToken(crypto, refreshTokenBytes, OAuthRefreshToken, "refresh");
|
|
619
|
+
const refreshTokenHash = yield* hashOpaqueToken(crypto, refreshToken);
|
|
620
|
+
const refreshTokenRecord = {
|
|
621
|
+
...baseRecord,
|
|
622
|
+
tokenHash: refreshTokenHash,
|
|
623
|
+
tokenType: "refresh_token",
|
|
624
|
+
expiresAt: refreshTokenExpiresAt,
|
|
625
|
+
};
|
|
626
|
+
return {
|
|
627
|
+
tokens: {
|
|
628
|
+
accessToken,
|
|
629
|
+
tokenType: "Bearer",
|
|
630
|
+
expiresIn: Math.floor((Number(accessTokenExpiresAt) - Number(input.now)) / 1000),
|
|
631
|
+
refreshToken,
|
|
632
|
+
...(input.scopes.length === 0
|
|
633
|
+
? {}
|
|
634
|
+
: { scopes: [...input.scopes] }),
|
|
635
|
+
},
|
|
636
|
+
tokenRecords: [accessTokenRecord, refreshTokenRecord],
|
|
637
|
+
};
|
|
638
|
+
}),
|
|
639
|
+
});
|
|
640
|
+
});
|
|
641
|
+
})(OAuthDeviceProviderModeOpaqueTokenIssuer || (OAuthDeviceProviderModeOpaqueTokenIssuer = {}));
|
|
642
|
+
const initialMemoryStoreState = (records) => {
|
|
643
|
+
const recordsById = new Map();
|
|
644
|
+
const idsByDeviceCodeHash = new Map();
|
|
645
|
+
const idsByUserCodeHash = new Map();
|
|
646
|
+
for (const source of records) {
|
|
647
|
+
if (recordsById.has(source.id) ||
|
|
648
|
+
idsByDeviceCodeHash.has(source.deviceCodeHash) ||
|
|
649
|
+
idsByUserCodeHash.has(source.userCodeHash)) {
|
|
650
|
+
continue;
|
|
651
|
+
}
|
|
652
|
+
const record = cloneRecord(source);
|
|
653
|
+
recordsById.set(record.id, record);
|
|
654
|
+
idsByDeviceCodeHash.set(record.deviceCodeHash, record.id);
|
|
655
|
+
idsByUserCodeHash.set(record.userCodeHash, record.id);
|
|
656
|
+
}
|
|
657
|
+
return { recordsById, idsByDeviceCodeHash, idsByUserCodeHash };
|
|
658
|
+
};
|
|
659
|
+
const memoryStoreRecordByHash = (state, index, hash) => {
|
|
660
|
+
const id = index.get(hash);
|
|
661
|
+
const record = id === undefined ? undefined : state.recordsById.get(id);
|
|
662
|
+
return record === undefined
|
|
663
|
+
? Option.none()
|
|
664
|
+
: Option.some(cloneRecord(record));
|
|
665
|
+
};
|
|
666
|
+
const memoryStoreWithRecord = (state, record) => {
|
|
667
|
+
const recordsById = new Map([
|
|
668
|
+
...state.recordsById,
|
|
669
|
+
[record.id, record],
|
|
670
|
+
]);
|
|
671
|
+
return { ...state, recordsById };
|
|
672
|
+
};
|
|
673
|
+
export var OAuthDeviceAuthorizationStoreMemory;
|
|
674
|
+
(function (OAuthDeviceAuthorizationStoreMemory) {
|
|
675
|
+
OAuthDeviceAuthorizationStoreMemory.make = (records = []) => Effect.gen(function* () {
|
|
676
|
+
const state = yield* Ref.make(initialMemoryStoreState(records));
|
|
677
|
+
return OAuthDeviceAuthorizationStore.of({
|
|
678
|
+
insert: (source) => Ref.modify(state, (current) => {
|
|
679
|
+
if (current.recordsById.has(source.id) ||
|
|
680
|
+
current.idsByDeviceCodeHash.has(source.deviceCodeHash) ||
|
|
681
|
+
current.idsByUserCodeHash.has(source.userCodeHash)) {
|
|
682
|
+
return [false, current];
|
|
683
|
+
}
|
|
684
|
+
const record = clonePendingRecord(source);
|
|
685
|
+
const recordsById = new Map(current.recordsById);
|
|
686
|
+
const idsByDeviceCodeHash = new Map(current.idsByDeviceCodeHash);
|
|
687
|
+
const idsByUserCodeHash = new Map(current.idsByUserCodeHash);
|
|
688
|
+
recordsById.set(record.id, record);
|
|
689
|
+
idsByDeviceCodeHash.set(record.deviceCodeHash, record.id);
|
|
690
|
+
idsByUserCodeHash.set(record.userCodeHash, record.id);
|
|
691
|
+
return [
|
|
692
|
+
true,
|
|
693
|
+
{ recordsById, idsByDeviceCodeHash, idsByUserCodeHash },
|
|
694
|
+
];
|
|
695
|
+
}),
|
|
696
|
+
findByDeviceCodeHash: (input) => Ref.get(state).pipe(Effect.map((current) => memoryStoreRecordByHash(current, current.idsByDeviceCodeHash, input.deviceCodeHash))),
|
|
697
|
+
findByUserCodeHash: (input) => Ref.get(state).pipe(Effect.map((current) => memoryStoreRecordByHash(current, current.idsByUserCodeHash, input.userCodeHash))),
|
|
698
|
+
findById: (id) => Ref.get(state).pipe(Effect.map((current) => {
|
|
699
|
+
const record = current.recordsById.get(id);
|
|
700
|
+
return record === undefined
|
|
701
|
+
? Option.none()
|
|
702
|
+
: Option.some(cloneRecord(record));
|
|
703
|
+
})),
|
|
704
|
+
decide: (input) => Ref.modify(state, (current) => {
|
|
705
|
+
const record = current.recordsById.get(input.id);
|
|
706
|
+
if (record === undefined) {
|
|
707
|
+
return [{ status: "missing" }, current];
|
|
708
|
+
}
|
|
709
|
+
if (Number(input.decidedAt) >= Number(record.expiresAt)) {
|
|
710
|
+
return [
|
|
711
|
+
{ status: "expired", record: cloneRecord(record) },
|
|
712
|
+
current,
|
|
713
|
+
];
|
|
714
|
+
}
|
|
715
|
+
if (record.status !== "pending") {
|
|
716
|
+
return [
|
|
717
|
+
{
|
|
718
|
+
status: "already_decided",
|
|
719
|
+
record: record.status === "approved"
|
|
720
|
+
? cloneApprovedRecord(record)
|
|
721
|
+
: cloneDeniedRecord(record),
|
|
722
|
+
},
|
|
723
|
+
current,
|
|
724
|
+
];
|
|
725
|
+
}
|
|
726
|
+
if (input.decision === "approve") {
|
|
727
|
+
const approved = {
|
|
728
|
+
...record,
|
|
729
|
+
status: "approved",
|
|
730
|
+
subject: input.subject,
|
|
731
|
+
grantedScopes: [...input.grantedScopes],
|
|
732
|
+
approvedAt: input.decidedAt,
|
|
733
|
+
...(input.metadata === undefined
|
|
734
|
+
? {}
|
|
735
|
+
: { metadata: cloneMetadata(input.metadata) }),
|
|
736
|
+
};
|
|
737
|
+
return [
|
|
738
|
+
{
|
|
739
|
+
status: "approved",
|
|
740
|
+
record: cloneApprovedRecord(approved),
|
|
741
|
+
},
|
|
742
|
+
memoryStoreWithRecord(current, approved),
|
|
743
|
+
];
|
|
744
|
+
}
|
|
745
|
+
const denied = {
|
|
746
|
+
...record,
|
|
747
|
+
status: "denied",
|
|
748
|
+
deniedAt: input.decidedAt,
|
|
749
|
+
...(input.metadata === undefined
|
|
750
|
+
? {}
|
|
751
|
+
: { metadata: cloneMetadata(input.metadata) }),
|
|
752
|
+
};
|
|
753
|
+
return [
|
|
754
|
+
{ status: "denied", record: cloneDeniedRecord(denied) },
|
|
755
|
+
memoryStoreWithRecord(current, denied),
|
|
756
|
+
];
|
|
757
|
+
}),
|
|
758
|
+
poll: (input) => Ref.modify(state, (current) => {
|
|
759
|
+
const id = current.idsByDeviceCodeHash.get(input.deviceCodeHash);
|
|
760
|
+
const record = id === undefined ? undefined : current.recordsById.get(id);
|
|
761
|
+
if (record === undefined || record.clientId !== input.clientId) {
|
|
762
|
+
return [{ status: "missing" }, current];
|
|
763
|
+
}
|
|
764
|
+
if (record.status === "approved" &&
|
|
765
|
+
record.consumedAt !== undefined) {
|
|
766
|
+
return [
|
|
767
|
+
{
|
|
768
|
+
status: "already_consumed",
|
|
769
|
+
record: cloneConsumedRecord(record),
|
|
770
|
+
},
|
|
771
|
+
current,
|
|
772
|
+
];
|
|
773
|
+
}
|
|
774
|
+
if (Number(input.now) >= Number(record.expiresAt)) {
|
|
775
|
+
return [
|
|
776
|
+
{ status: "expired", record: cloneRecord(record) },
|
|
777
|
+
current,
|
|
778
|
+
];
|
|
779
|
+
}
|
|
780
|
+
if (record.status === "denied") {
|
|
781
|
+
return [
|
|
782
|
+
{
|
|
783
|
+
status: "denied",
|
|
784
|
+
record: cloneDeniedRecord(record),
|
|
785
|
+
},
|
|
786
|
+
current,
|
|
787
|
+
];
|
|
788
|
+
}
|
|
789
|
+
if (record.status === "approved") {
|
|
790
|
+
return [
|
|
791
|
+
{
|
|
792
|
+
status: "approved",
|
|
793
|
+
record: cloneUnconsumedApprovedRecord(record),
|
|
794
|
+
},
|
|
795
|
+
current,
|
|
796
|
+
];
|
|
797
|
+
}
|
|
798
|
+
if (Number(input.now) < Number(record.nextPollAt)) {
|
|
799
|
+
const interval = record.pollIntervalSeconds +
|
|
800
|
+
oauthDeviceAuthorizationSlowDownSeconds;
|
|
801
|
+
const slowed = {
|
|
802
|
+
...record,
|
|
803
|
+
pollIntervalSeconds: interval,
|
|
804
|
+
lastPolledAt: input.now,
|
|
805
|
+
nextPollAt: UnixMillis(Number(input.now) + interval * 1000),
|
|
806
|
+
};
|
|
807
|
+
return [
|
|
808
|
+
{
|
|
809
|
+
status: "slow_down",
|
|
810
|
+
record: cloneRecord(slowed),
|
|
811
|
+
interval,
|
|
812
|
+
},
|
|
813
|
+
memoryStoreWithRecord(current, slowed),
|
|
814
|
+
];
|
|
815
|
+
}
|
|
816
|
+
const pending = {
|
|
817
|
+
...record,
|
|
818
|
+
lastPolledAt: input.now,
|
|
819
|
+
nextPollAt: UnixMillis(Number(input.now) + record.pollIntervalSeconds * 1000),
|
|
820
|
+
};
|
|
821
|
+
return [
|
|
822
|
+
{
|
|
823
|
+
status: "pending",
|
|
824
|
+
record: clonePendingRecord(pending),
|
|
825
|
+
},
|
|
826
|
+
memoryStoreWithRecord(current, pending),
|
|
827
|
+
];
|
|
828
|
+
}),
|
|
829
|
+
deleteExpired: (input) => Ref.modify(state, (current) => {
|
|
830
|
+
const recordsById = new Map(current.recordsById);
|
|
831
|
+
const idsByDeviceCodeHash = new Map(current.idsByDeviceCodeHash);
|
|
832
|
+
const idsByUserCodeHash = new Map(current.idsByUserCodeHash);
|
|
833
|
+
let deleted = 0;
|
|
834
|
+
for (const [id, record] of current.recordsById) {
|
|
835
|
+
if (Number(record.expiresAt) > Number(input.beforeExpiresAt)) {
|
|
836
|
+
continue;
|
|
837
|
+
}
|
|
838
|
+
recordsById.delete(id);
|
|
839
|
+
idsByDeviceCodeHash.delete(record.deviceCodeHash);
|
|
840
|
+
idsByUserCodeHash.delete(record.userCodeHash);
|
|
841
|
+
deleted += 1;
|
|
842
|
+
}
|
|
843
|
+
return [
|
|
844
|
+
deleted,
|
|
845
|
+
{ recordsById, idsByDeviceCodeHash, idsByUserCodeHash },
|
|
846
|
+
];
|
|
847
|
+
}),
|
|
848
|
+
});
|
|
849
|
+
});
|
|
850
|
+
})(OAuthDeviceAuthorizationStoreMemory || (OAuthDeviceAuthorizationStoreMemory = {}));
|
|
851
|
+
export const OAuthDeviceAuthorizationStoreMemoryLive = (records = []) => Layer.effect(OAuthDeviceAuthorizationStore)(OAuthDeviceAuthorizationStoreMemory.make(records));
|
|
852
|
+
export const OAuthDeviceAuthorizationLive = (options) => Layer.effect(OAuthDeviceAuthorization)(OAuthDeviceAuthorization.make(options));
|
|
853
|
+
export const OAuthDeviceCodeTokenGrantLive = Layer.effect(OAuthDeviceCodeTokenGrant)(OAuthDeviceCodeTokenGrant.make());
|
|
854
|
+
export const OAuthDeviceProviderModeOpaqueTokenIssuerLive = (options = {}) => Layer.effect(OAuthDeviceCodeTokenIssuer, OAuthDeviceProviderModeOpaqueTokenIssuer.make(options));
|
|
855
|
+
export const startOAuthDeviceAuthorization = (input) => Effect.flatMap(OAuthDeviceAuthorization, (service) => service.start(input));
|
|
856
|
+
export const inspectOAuthDeviceAuthorization = (input) => Effect.flatMap(OAuthDeviceAuthorization, (service) => service.inspect(input));
|
|
857
|
+
export const approveOAuthDeviceAuthorization = (input) => Effect.flatMap(OAuthDeviceAuthorization, (service) => service.approve(input));
|
|
858
|
+
export const denyOAuthDeviceAuthorization = (input) => Effect.flatMap(OAuthDeviceAuthorization, (service) => service.deny(input));
|
|
859
|
+
export const exchangeOAuthDeviceCode = (input) => Effect.flatMap(OAuthDeviceCodeTokenGrant, (service) => service.exchange(input));
|
|
860
|
+
//# sourceMappingURL=OAuthDeviceAuthorization.js.map
|