| ... | @@ -88,22 +88,38 @@ export class ActivePairingLinksLoadError extends Schema.TaggedErrorClass<ActiveP | ... | @@ -88,22 +88,38 @@ export class ActivePairingLinksLoadError extends Schema.TaggedErrorClass<ActiveP |
| 88 | export class PairingLinkRevokeError extends Schema.TaggedErrorClass<PairingLinkRevokeError>()( | 88 | export class PairingLinkRevokeError extends Schema.TaggedErrorClass<PairingLinkRevokeError>()( |
| 89 | "PairingLinkRevokeError", | 89 | "PairingLinkRevokeError", |
| 90 | { | 90 | { |
| | 91 | pairingLinkId: Schema.String, |
| 91 | cause: Schema.Defect(), | 92 | cause: Schema.Defect(), |
| 92 | }, | 93 | }, |
| 93 | ) { | 94 | ) { |
| 94 | override get message(): string { | 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 | export class PairingCredentialIssueError extends Schema.TaggedErrorClass<PairingCredentialIssueError>()( | 100 | export class PairingCredentialIssueError extends Schema.TaggedErrorClass<PairingCredentialIssueError>()( |
| 100 | "PairingCredentialIssueError", | 101 | "PairingCredentialIssueError", |
| 101 | { | 102 | { |
| | 103 | pairingLinkId: Schema.String, |
| | 104 | subject: Schema.String, |
| | 105 | label: Schema.optional(Schema.String), |
| 102 | cause: Schema.Defect(), | 106 | cause: Schema.Defect(), |
| 103 | }, | 107 | }, |
| 104 | ) { | 108 | ) { |
| 105 | override get message(): string { | 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,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 | export const BootstrapCredentialInternalError = Schema.Union([ | 159 | export const BootstrapCredentialInternalError = Schema.Union([ |
| 122 | ActivePairingLinksLoadError, | 160 | ActivePairingLinksLoadError, |
| 123 | PairingLinkRevokeError, | 161 | PairingLinkRevokeError, |
| 124 | PairingCredentialIssueError, | 162 | PairingCredentialIssueError, |
| | 163 | PairingCredentialRandomGenerationError, |
| 125 | BootstrapCredentialConsumeError, | 164 | BootstrapCredentialConsumeError, |
| | 165 | BootstrapCredentialConsumeAvailableError, |
| | 166 | BootstrapCredentialLookupError, |
| 126 | ]); | 167 | ]); |
| 127 | export type BootstrapCredentialInternalError = typeof BootstrapCredentialInternalError.Type; | 168 | export type BootstrapCredentialInternalError = typeof BootstrapCredentialInternalError.Type; |
| 128 | export const isBootstrapCredentialInternalError = Schema.is(BootstrapCredentialInternalError); | 169 | export const isBootstrapCredentialInternalError = Schema.is(BootstrapCredentialInternalError); |
| ... | @@ -207,7 +248,14 @@ export const make = Effect.gen(function* () { | ... | @@ -207,7 +248,14 @@ export const make = Effect.gen(function* () { |
| 207 | const generatePairingToken = Effect.gen(function* () { | 248 | const generatePairingToken = Effect.gen(function* () { |
| 208 | let credential = ""; | 249 | let credential = ""; |
| 209 | while (credential.length < PAIRING_TOKEN_LENGTH) { | 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 | for (const byte of bytes) { | 259 | for (const byte of bytes) { |
| 212 | if (byte >= PAIRING_TOKEN_REJECTION_LIMIT) { | 260 | if (byte >= PAIRING_TOKEN_REJECTION_LIMIT) { |
| 213 | continue; | 261 | continue; |
| ... | @@ -287,58 +335,73 @@ export const make = Effect.gen(function* () { | ... | @@ -287,58 +335,73 @@ export const make = Effect.gen(function* () { |
| 287 | const revoke: PairingGrantStore["Service"]["revoke"] = Effect.fn("PairingGrantStore.revoke")( | 335 | const revoke: PairingGrantStore["Service"]["revoke"] = Effect.fn("PairingGrantStore.revoke")( |
| 288 | function* (id) { | 336 | function* (id) { |
| 289 | const revokedAt = yield* DateTime.now; | 337 | const revokedAt = yield* DateTime.now; |
| 290 | const revoked = yield* pairingLinks.revoke({ | 338 | const revoked = yield* pairingLinks |
| 291 | id, | 339 | .revoke({ |
| 292 | revokedAt, | 340 | id, |
| 293 | }); | 341 | revokedAt, |
| | 342 | }) |
| | 343 | .pipe(Effect.mapError((cause) => new PairingLinkRevokeError({ pairingLinkId: id, cause }))); |
| 294 | if (revoked) { | 344 | if (revoked) { |
| 295 | yield* emitRemoved(id); | 345 | yield* emitRemoved(id); |
| 296 | } | 346 | } |
| 297 | return revoked; | 347 | return revoked; |
| 298 | }, | 348 | }, |
| 299 | Effect.mapError((cause) => new PairingLinkRevokeError({ cause })), | | |
| 300 | ); | 349 | ); |
| 301 | | 350 | |
| 302 | const issueOneTimeToken: PairingGrantStore["Service"]["issueOneTimeToken"] = Effect.fn( | 351 | const issueOneTimeToken: PairingGrantStore["Service"]["issueOneTimeToken"] = Effect.fn( |
| 303 | "PairingGrantStore.issueOneTimeToken", | 352 | "PairingGrantStore.issueOneTimeToken", |
| 304 | )( | 353 | )(function* (input) { |
| 305 | function* (input) { | 354 | const id = yield* crypto.randomUUIDv4.pipe( |
| 306 | const id = yield* crypto.randomUUIDv4; | 355 | Effect.mapError( |
| 307 | const credential = yield* generatePairingToken; | 356 | (cause) => new PairingCredentialRandomGenerationError({ operation: "generate-id", cause }), |
| 308 | const ttl = input?.ttl ?? DEFAULT_ONE_TIME_TOKEN_TTL_MINUTES; | 357 | ), |
| 309 | const now = yield* DateTime.now; | 358 | ); |
| 310 | const expiresAt = DateTime.add(now, { milliseconds: Duration.toMillis(ttl) }); | 359 | const credential = yield* generatePairingToken; |
| 311 | const issued: IssuedBootstrapCredential = { | 360 | const ttl = input?.ttl ?? DEFAULT_ONE_TIME_TOKEN_TTL_MINUTES; |
| 312 | id, | 361 | const now = yield* DateTime.now; |
| 313 | credential, | 362 | const expiresAt = DateTime.add(now, { milliseconds: Duration.toMillis(ttl) }); |
| 314 | ...(input?.label ? { label: input.label } : {}), | 363 | const issued: IssuedBootstrapCredential = { |
| 315 | ...(input?.proofKeyThumbprint ? { proofKeyThumbprint: input.proofKeyThumbprint } : {}), | 364 | id, |
| 316 | expiresAt, | 365 | credential, |
| 317 | }; | 366 | ...(input?.label ? { label: input.label } : {}), |
| 318 | yield* pairingLinks.create({ | 367 | ...(input?.proofKeyThumbprint ? { proofKeyThumbprint: input.proofKeyThumbprint } : {}), |
| | 368 | expiresAt, |
| | 369 | }; |
| | 370 | const subject = input?.subject ?? "one-time-token"; |
| | 371 | yield* pairingLinks |
| | 372 | .create({ |
| 319 | id, | 373 | id, |
| 320 | credential, | 374 | credential, |
| 321 | method: "one-time-token", | 375 | method: "one-time-token", |
| 322 | scopes: input?.scopes ?? AuthStandardClientScopes, | 376 | scopes: input?.scopes ?? AuthStandardClientScopes, |
| 323 | subject: input?.subject ?? "one-time-token", | 377 | subject, |
| 324 | label: input?.label ?? null, | 378 | label: input?.label ?? null, |
| 325 | proofKeyThumbprint: input?.proofKeyThumbprint ?? null, | 379 | proofKeyThumbprint: input?.proofKeyThumbprint ?? null, |
| 326 | createdAt: now, | 380 | createdAt: now, |
| 327 | expiresAt: expiresAt, | 381 | expiresAt: expiresAt, |
| 328 | }); | 382 | }) |
| 329 | yield* emitUpsert({ | 383 | .pipe( |
| 330 | id, | 384 | Effect.mapError( |
| 331 | credential, | 385 | (cause) => |
| 332 | scopes: input?.scopes ?? AuthStandardClientScopes, | 386 | new PairingCredentialIssueError({ |
| 333 | subject: input?.subject ?? "one-time-token", | 387 | pairingLinkId: id, |
| 334 | ...(input?.label ? { label: input.label } : {}), | 388 | subject, |
| 335 | createdAt: now, | 389 | ...(input?.label ? { label: input.label } : {}), |
| 336 | expiresAt, | 390 | cause, |
| 337 | }); | 391 | }), |
| 338 | return issued; | 392 | ), |
| 339 | }, | 393 | ); |
| 340 | Effect.mapError((cause) => new PairingCredentialIssueError({ cause })), | 394 | yield* emitUpsert({ |
| 341 | ); | 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 | const consume: PairingGrantStore["Service"]["consume"] = Effect.fn("PairingGrantStore.consume")( | 406 | const consume: PairingGrantStore["Service"]["consume"] = Effect.fn("PairingGrantStore.consume")( |
| 344 | function* (credential, input) { | 407 | function* (credential, input) { |
| ... | @@ -420,12 +483,14 @@ export const make = Effect.gen(function* () { | ... | @@ -420,12 +483,14 @@ export const make = Effect.gen(function* () { |
| 420 | return yield* seededResult.error; | 483 | return yield* seededResult.error; |
| 421 | } | 484 | } |
| 422 | | 485 | |
| 423 | const consumed = yield* pairingLinks.consumeAvailable({ | 486 | const consumed = yield* pairingLinks |
| 424 | credential, | 487 | .consumeAvailable({ |
| 425 | proofKeyThumbprint: input?.proofKeyThumbprint ?? null, | 488 | credential, |
| 426 | consumedAt: now, | 489 | proofKeyThumbprint: input?.proofKeyThumbprint ?? null, |
| 427 | now, | 490 | consumedAt: now, |
| 428 | }); | 491 | now, |
| | 492 | }) |
| | 493 | .pipe(Effect.mapError((cause) => new BootstrapCredentialConsumeAvailableError({ cause }))); |
| 429 | | 494 | |
| 430 | if (Option.isSome(consumed)) { | 495 | if (Option.isSome(consumed)) { |
| 431 | yield* emitRemoved(consumed.value.id); | 496 | yield* emitRemoved(consumed.value.id); |
| ... | @@ -441,7 +506,9 @@ export const make = Effect.gen(function* () { | ... | @@ -441,7 +506,9 @@ export const make = Effect.gen(function* () { |
| 441 | } satisfies BootstrapGrant; | 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 | if (Option.isNone(matching)) { | 512 | if (Option.isNone(matching)) { |
| 446 | return yield* new UnknownBootstrapCredentialError({}); | 513 | return yield* new UnknownBootstrapCredentialError({}); |
| 447 | } | 514 | } |
| ... | @@ -467,9 +534,6 @@ export const make = Effect.gen(function* () { | ... | @@ -467,9 +534,6 @@ export const make = Effect.gen(function* () { |
| 467 | | 534 | |
| 468 | return yield* new UnavailableBootstrapCredentialError({}); | 535 | return yield* new UnavailableBootstrapCredentialError({}); |
| 469 | }, | 536 | }, |
| 470 | Effect.mapError((cause) => | | |
| 471 | isBootstrapCredentialError(cause) ? cause : new BootstrapCredentialConsumeError({ cause }), | | |
| 472 | ), | | |
| 473 | ); | 537 | ); |
| 474 | | 538 | |
| 475 | return PairingGrantStore.of({ | 539 | return PairingGrantStore.of({ |