| ... | ... | @@ -88,22 +88,38 @@ export class ActivePairingLinksLoadError extends Schema.TaggedErrorClass<ActiveP |
| 88 | 88 | export class PairingLinkRevokeError extends Schema.TaggedErrorClass<PairingLinkRevokeError>()( |
| 89 | 89 | "PairingLinkRevokeError", |
| 90 | 90 | { |
| 91 | pairingLinkId: Schema.String, |
| 91 | 92 | cause: Schema.Defect(), |
| 92 | 93 | }, |
| 93 | 94 | ) { |
| 94 | 95 | override get message(): string { |
| 95 | | return "Failed to revoke pairing link."; |
| 96 | return `Failed to revoke pairing link '${this.pairingLinkId}'.`; |
| 96 | 97 | } |
| 97 | 98 | } |
| 98 | 99 | |
| 99 | 100 | export class PairingCredentialIssueError extends Schema.TaggedErrorClass<PairingCredentialIssueError>()( |
| 100 | 101 | "PairingCredentialIssueError", |
| 101 | 102 | { |
| 103 | pairingLinkId: Schema.String, |
| 104 | subject: Schema.String, |
| 105 | label: Schema.optional(Schema.String), |
| 102 | 106 | cause: Schema.Defect(), |
| 103 | 107 | }, |
| 104 | 108 | ) { |
| 105 | 109 | override get message(): string { |
| 106 | | return "Failed to issue pairing credential."; |
| 110 | return `Failed to issue pairing credential '${this.pairingLinkId}' for '${this.subject}'.`; |
| 111 | } |
| 112 | } |
| 113 | |
| 114 | export class PairingCredentialRandomGenerationError extends Schema.TaggedErrorClass<PairingCredentialRandomGenerationError>()( |
| 115 | "PairingCredentialRandomGenerationError", |
| 116 | { |
| 117 | operation: Schema.Literals(["generate-id", "generate-token"]), |
| 118 | cause: Schema.Defect(), |
| 119 | }, |
| 120 | ) { |
| 121 | override get message(): string { |
| 122 | return `Failed to generate pairing credential data during '${this.operation}'.`; |
| 107 | 123 | } |
| 108 | 124 | } |
| 109 | 125 | |
| ... | ... | @@ -118,11 +134,36 @@ export class BootstrapCredentialConsumeError extends Schema.TaggedErrorClass<Boo |
| 118 | 134 | } |
| 119 | 135 | } |
| 120 | 136 | |
| 137 | export class BootstrapCredentialConsumeAvailableError extends Schema.TaggedErrorClass<BootstrapCredentialConsumeAvailableError>()( |
| 138 | "BootstrapCredentialConsumeAvailableError", |
| 139 | { |
| 140 | cause: Schema.Defect(), |
| 141 | }, |
| 142 | ) { |
| 143 | override get message(): string { |
| 144 | return "Failed to atomically consume an available bootstrap credential."; |
| 145 | } |
| 146 | } |
| 147 | |
| 148 | export class BootstrapCredentialLookupError extends Schema.TaggedErrorClass<BootstrapCredentialLookupError>()( |
| 149 | "BootstrapCredentialLookupError", |
| 150 | { |
| 151 | cause: Schema.Defect(), |
| 152 | }, |
| 153 | ) { |
| 154 | override get message(): string { |
| 155 | return "Failed to look up bootstrap credential state."; |
| 156 | } |
| 157 | } |
| 158 | |
| 121 | 159 | export const BootstrapCredentialInternalError = Schema.Union([ |
| 122 | 160 | ActivePairingLinksLoadError, |
| 123 | 161 | PairingLinkRevokeError, |
| 124 | 162 | PairingCredentialIssueError, |
| 163 | PairingCredentialRandomGenerationError, |
| 125 | 164 | BootstrapCredentialConsumeError, |
| 165 | BootstrapCredentialConsumeAvailableError, |
| 166 | BootstrapCredentialLookupError, |
| 126 | 167 | ]); |
| 127 | 168 | export type BootstrapCredentialInternalError = typeof BootstrapCredentialInternalError.Type; |
| 128 | 169 | export const isBootstrapCredentialInternalError = Schema.is(BootstrapCredentialInternalError); |
| ... | ... | @@ -207,7 +248,14 @@ export const make = Effect.gen(function* () { |
| 207 | 248 | const generatePairingToken = Effect.gen(function* () { |
| 208 | 249 | let credential = ""; |
| 209 | 250 | while (credential.length < PAIRING_TOKEN_LENGTH) { |
| 210 | | const bytes = yield* crypto.randomBytes(PAIRING_TOKEN_LENGTH); |
| 251 | const bytes = yield* crypto |
| 252 | .randomBytes(PAIRING_TOKEN_LENGTH) |
| 253 | .pipe( |
| 254 | Effect.mapError( |
| 255 | (cause) => |
| 256 | new PairingCredentialRandomGenerationError({ operation: "generate-token", cause }), |
| 257 | ), |
| 258 | ); |
| 211 | 259 | for (const byte of bytes) { |
| 212 | 260 | if (byte >= PAIRING_TOKEN_REJECTION_LIMIT) { |
| 213 | 261 | continue; |
| ... | ... | @@ -287,58 +335,73 @@ export const make = Effect.gen(function* () { |
| 287 | 335 | const revoke: PairingGrantStore["Service"]["revoke"] = Effect.fn("PairingGrantStore.revoke")( |
| 288 | 336 | function* (id) { |
| 289 | 337 | const revokedAt = yield* DateTime.now; |
| 290 | | const revoked = yield* pairingLinks.revoke({ |
| 291 | | id, |
| 292 | | revokedAt, |
| 293 | | }); |
| 338 | const revoked = yield* pairingLinks |
| 339 | .revoke({ |
| 340 | id, |
| 341 | revokedAt, |
| 342 | }) |
| 343 | .pipe(Effect.mapError((cause) => new PairingLinkRevokeError({ pairingLinkId: id, cause }))); |
| 294 | 344 | if (revoked) { |
| 295 | 345 | yield* emitRemoved(id); |
| 296 | 346 | } |
| 297 | 347 | return revoked; |
| 298 | 348 | }, |
| 299 | | Effect.mapError((cause) => new PairingLinkRevokeError({ cause })), |
| 300 | 349 | ); |
| 301 | 350 | |
| 302 | 351 | const issueOneTimeToken: PairingGrantStore["Service"]["issueOneTimeToken"] = Effect.fn( |
| 303 | 352 | "PairingGrantStore.issueOneTimeToken", |
| 304 | | )( |
| 305 | | function* (input) { |
| 306 | | const id = yield* crypto.randomUUIDv4; |
| 307 | | const credential = yield* generatePairingToken; |
| 308 | | const ttl = input?.ttl ?? DEFAULT_ONE_TIME_TOKEN_TTL_MINUTES; |
| 309 | | const now = yield* DateTime.now; |
| 310 | | const expiresAt = DateTime.add(now, { milliseconds: Duration.toMillis(ttl) }); |
| 311 | | const issued: IssuedBootstrapCredential = { |
| 312 | | id, |
| 313 | | credential, |
| 314 | | ...(input?.label ? { label: input.label } : {}), |
| 315 | | ...(input?.proofKeyThumbprint ? { proofKeyThumbprint: input.proofKeyThumbprint } : {}), |
| 316 | | expiresAt, |
| 317 | | }; |
| 318 | | yield* pairingLinks.create({ |
| 353 | )(function* (input) { |
| 354 | const id = yield* crypto.randomUUIDv4.pipe( |
| 355 | Effect.mapError( |
| 356 | (cause) => new PairingCredentialRandomGenerationError({ operation: "generate-id", cause }), |
| 357 | ), |
| 358 | ); |
| 359 | const credential = yield* generatePairingToken; |
| 360 | const ttl = input?.ttl ?? DEFAULT_ONE_TIME_TOKEN_TTL_MINUTES; |
| 361 | const now = yield* DateTime.now; |
| 362 | const expiresAt = DateTime.add(now, { milliseconds: Duration.toMillis(ttl) }); |
| 363 | const issued: IssuedBootstrapCredential = { |
| 364 | id, |
| 365 | credential, |
| 366 | ...(input?.label ? { label: input.label } : {}), |
| 367 | ...(input?.proofKeyThumbprint ? { proofKeyThumbprint: input.proofKeyThumbprint } : {}), |
| 368 | expiresAt, |
| 369 | }; |
| 370 | const subject = input?.subject ?? "one-time-token"; |
| 371 | yield* pairingLinks |
| 372 | .create({ |
| 319 | 373 | id, |
| 320 | 374 | credential, |
| 321 | 375 | method: "one-time-token", |
| 322 | 376 | scopes: input?.scopes ?? AuthStandardClientScopes, |
| 323 | | subject: input?.subject ?? "one-time-token", |
| 377 | subject, |
| 324 | 378 | label: input?.label ?? null, |
| 325 | 379 | proofKeyThumbprint: input?.proofKeyThumbprint ?? null, |
| 326 | 380 | createdAt: now, |
| 327 | 381 | expiresAt: expiresAt, |
| 328 | | }); |
| 329 | | yield* emitUpsert({ |
| 330 | | id, |
| 331 | | credential, |
| 332 | | scopes: input?.scopes ?? AuthStandardClientScopes, |
| 333 | | subject: input?.subject ?? "one-time-token", |
| 334 | | ...(input?.label ? { label: input.label } : {}), |
| 335 | | createdAt: now, |
| 336 | | expiresAt, |
| 337 | | }); |
| 338 | | return issued; |
| 339 | | }, |
| 340 | | Effect.mapError((cause) => new PairingCredentialIssueError({ cause })), |
| 341 | | ); |
| 382 | }) |
| 383 | .pipe( |
| 384 | Effect.mapError( |
| 385 | (cause) => |
| 386 | new PairingCredentialIssueError({ |
| 387 | pairingLinkId: id, |
| 388 | subject, |
| 389 | ...(input?.label ? { label: input.label } : {}), |
| 390 | cause, |
| 391 | }), |
| 392 | ), |
| 393 | ); |
| 394 | yield* emitUpsert({ |
| 395 | id, |
| 396 | credential, |
| 397 | scopes: input?.scopes ?? AuthStandardClientScopes, |
| 398 | subject: input?.subject ?? "one-time-token", |
| 399 | ...(input?.label ? { label: input.label } : {}), |
| 400 | createdAt: now, |
| 401 | expiresAt, |
| 402 | }); |
| 403 | return issued; |
| 404 | }); |
| 342 | 405 | |
| 343 | 406 | const consume: PairingGrantStore["Service"]["consume"] = Effect.fn("PairingGrantStore.consume")( |
| 344 | 407 | function* (credential, input) { |
| ... | ... | @@ -420,12 +483,14 @@ export const make = Effect.gen(function* () { |
| 420 | 483 | return yield* seededResult.error; |
| 421 | 484 | } |
| 422 | 485 | |
| 423 | | const consumed = yield* pairingLinks.consumeAvailable({ |
| 424 | | credential, |
| 425 | | proofKeyThumbprint: input?.proofKeyThumbprint ?? null, |
| 426 | | consumedAt: now, |
| 427 | | now, |
| 428 | | }); |
| 486 | const consumed = yield* pairingLinks |
| 487 | .consumeAvailable({ |
| 488 | credential, |
| 489 | proofKeyThumbprint: input?.proofKeyThumbprint ?? null, |
| 490 | consumedAt: now, |
| 491 | now, |
| 492 | }) |
| 493 | .pipe(Effect.mapError((cause) => new BootstrapCredentialConsumeAvailableError({ cause }))); |
| 429 | 494 | |
| 430 | 495 | if (Option.isSome(consumed)) { |
| 431 | 496 | yield* emitRemoved(consumed.value.id); |
| ... | ... | @@ -441,7 +506,9 @@ export const make = Effect.gen(function* () { |
| 441 | 506 | } satisfies BootstrapGrant; |
| 442 | 507 | } |
| 443 | 508 | |
| 444 | | const matching = yield* pairingLinks.getByCredential({ credential }); |
| 509 | const matching = yield* pairingLinks |
| 510 | .getByCredential({ credential }) |
| 511 | .pipe(Effect.mapError((cause) => new BootstrapCredentialLookupError({ cause }))); |
| 445 | 512 | if (Option.isNone(matching)) { |
| 446 | 513 | return yield* new UnknownBootstrapCredentialError({}); |
| 447 | 514 | } |
| ... | ... | @@ -467,9 +534,6 @@ export const make = Effect.gen(function* () { |
| 467 | 534 | |
| 468 | 535 | return yield* new UnavailableBootstrapCredentialError({}); |
| 469 | 536 | }, |
| 470 | | Effect.mapError((cause) => |
| 471 | | isBootstrapCredentialError(cause) ? cause : new BootstrapCredentialConsumeError({ cause }), |
| 472 | | ), |
| 473 | 537 | ); |
| 474 | 538 | |
| 475 | 539 | return PairingGrantStore.of({ |