| author | |
| committer | |
| log | 7b791895ad043e180c1d56595a08866e16d0766e |
| tree | 0f3b8206501f4d5d4ff6bd41c678033394a10482 |
| parent | 4c16c66368c934a3f23794fc4826a2c77fe9f323 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: codex <codex@users.noreply.github.com>9 files changed, 614 insertions(+), 209 deletions(-)
infra/relay/src/auth/DpopProofs.test.ts+26| ... | ... | @@ -96,4 +96,30 @@ describe("DpopProofReplay", () => { |
| 96 | 96 | Effect.provide(DpopProofs.layer.pipe(Layer.provide(Layer.succeed(RelayDb.RelayDb, fakeDb)))), |
| 97 | 97 | ); |
| 98 | 98 | }); |
| 99 | ||
| 100 | it.effect("retains the prune cutoff and database failure", () => { | |
| 101 | const cause = new Error("database unavailable"); | |
| 102 | const fakeDb = { | |
| 103 | delete: (table: unknown) => { | |
| 104 | expect(table).toBe(relayDpopProofs); | |
| 105 | return { | |
| 106 | where: () => Effect.fail(cause), | |
| 107 | }; | |
| 108 | }, | |
| 109 | } as unknown as RelayDb.RelayDb["Service"]; | |
| 110 | ||
| 111 | return Effect.gen(function* () { | |
| 112 | const replay = yield* DpopProofs.DpopProofReplay; | |
| 113 | const error = yield* Effect.flip(replay.pruneExpired); | |
| 114 | ||
| 115 | expect(error).toMatchObject({ | |
| 116 | _tag: "DpopProofReplayPersistenceError", | |
| 117 | operation: "prune-expired", | |
| 118 | }); | |
| 119 | expect(Date.parse(error.expiresBefore ?? "")).not.toBeNaN(); | |
| 120 | expect(error.cause).toBe(cause); | |
| 121 | }).pipe( | |
| 122 | Effect.provide(DpopProofs.layer.pipe(Layer.provide(Layer.succeed(RelayDb.RelayDb, fakeDb)))), | |
| 123 | ); | |
| 124 | }); | |
| 99 | 125 | }); |
infra/relay/src/auth/DpopProofs.ts+33-8| ... | ... | @@ -13,11 +13,16 @@ import { relayDpopProofs } from "../persistence/schema.ts"; |
| 13 | 13 | export class DpopProofReplayPersistenceError extends Schema.TaggedErrorClass<DpopProofReplayPersistenceError>()( |
| 14 | 14 | "DpopProofReplayPersistenceError", |
| 15 | 15 | { |
| 16 | operation: Schema.Literals(["consume", "prune-expired"]), | |
| 17 | thumbprint: Schema.optionalKey(Schema.String), | |
| 18 | jti: Schema.optionalKey(Schema.String), | |
| 19 | iat: Schema.optionalKey(Schema.Number), | |
| 20 | expiresBefore: Schema.optionalKey(Schema.String), | |
| 16 | 21 | cause: Schema.Defect(), |
| 17 | 22 | }, |
| 18 | 23 | ) { |
| 19 | 24 | override get message(): string { |
| 20 | return "Failed to persist DPoP proof replay state"; | |
| 25 | return `Failed to persist DPoP proof replay state during '${this.operation}'`; | |
| 21 | 26 | } |
| 22 | 27 | } |
| 23 | 28 | |
| ... | ... | @@ -58,10 +63,21 @@ const make = Effect.gen(function* () { |
| 58 | 63 | createdAt, |
| 59 | 64 | }) |
| 60 | 65 | .onConflictDoNothing() |
| 61 | .returning({ jti: relayDpopProofs.jti }); | |
| 66 | .returning({ jti: relayDpopProofs.jti }) | |
| 67 | .pipe( | |
| 68 | Effect.mapError( | |
| 69 | (cause) => | |
| 70 | new DpopProofReplayPersistenceError({ | |
| 71 | operation: "consume", | |
| 72 | thumbprint: input.thumbprint, | |
| 73 | jti: input.jti, | |
| 74 | iat: input.iat, | |
| 75 | cause, | |
| 76 | }), | |
| 77 | ), | |
| 78 | ); | |
| 62 | 79 | return inserted.length > 0; |
| 63 | 80 | }, |
| 64 | Effect.mapError((cause) => new DpopProofReplayPersistenceError({ cause })), | |
| 65 | 81 | ); |
| 66 | 82 | |
| 67 | 83 | const verifyAndConsume: DpopProofReplay["Service"]["verifyAndConsume"] = Effect.fn( |
| ... | ... | @@ -114,11 +130,20 @@ const make = Effect.gen(function* () { |
| 114 | 130 | const pruneExpired: DpopProofReplay["Service"]["pruneExpired"] = Effect.gen(function* () { |
| 115 | 131 | const now = DateTime.formatIso(yield* DateTime.now); |
| 116 | 132 | yield* Effect.annotateCurrentSpan({ "relay.dpop_prune.before": now }); |
| 117 | yield* db.delete(relayDpopProofs).where(lt(relayDpopProofs.expiresAt, now)); | |
| 118 | }).pipe( | |
| 119 | Effect.withSpan("relay.dpop_proofs.prune_expired"), | |
| 120 | Effect.mapError((cause) => new DpopProofReplayPersistenceError({ cause })), | |
| 121 | ); | |
| 133 | yield* db | |
| 134 | .delete(relayDpopProofs) | |
| 135 | .where(lt(relayDpopProofs.expiresAt, now)) | |
| 136 | .pipe( | |
| 137 | Effect.mapError( | |
| 138 | (cause) => | |
| 139 | new DpopProofReplayPersistenceError({ | |
| 140 | operation: "prune-expired", | |
| 141 | expiresBefore: now, | |
| 142 | cause, | |
| 143 | }), | |
| 144 | ), | |
| 145 | ); | |
| 146 | }).pipe(Effect.withSpan("relay.dpop_proofs.prune_expired")); | |
| 122 | 147 | |
| 123 | 148 | return DpopProofReplay.of({ |
| 124 | 149 | verifyAndConsume, |
infra/relay/src/auth/DpopProofs.verifyAndConsume.test.ts+11-3| ... | ... | @@ -163,7 +163,7 @@ describe("DpopProofReplay.verifyAndConsume", () => { |
| 163 | 163 | iat: Math.floor(now.epochMilliseconds / 1_000), |
| 164 | 164 | jti: "proof-persistence-failure", |
| 165 | 165 | }); |
| 166 | const cause = "database unavailable"; | |
| 166 | const cause = { _tag: "DatabaseUnavailable" } as const; | |
| 167 | 167 | |
| 168 | 168 | return Effect.gen(function* () { |
| 169 | 169 | const replay = yield* DpopProofs.DpopProofReplay; |
| ... | ... | @@ -177,8 +177,16 @@ describe("DpopProofReplay.verifyAndConsume", () => { |
| 177 | 177 | }), |
| 178 | 178 | ); |
| 179 | 179 | |
| 180 | expect(error).toEqual(new DpopProofs.DpopProofReplayPersistenceError({ cause })); | |
| 181 | }).pipe(Effect.provide(layer(() => Effect.fail({ _tag: cause })))); | |
| 180 | expect(error).toMatchObject({ | |
| 181 | _tag: "DpopProofReplayPersistenceError", | |
| 182 | operation: "consume", | |
| 183 | thumbprint: proof.thumbprint, | |
| 184 | jti: "proof-persistence-failure", | |
| 185 | iat: Math.floor(now.epochMilliseconds / 1_000), | |
| 186 | }); | |
| 187 | expect(error.cause).toBe(cause); | |
| 188 | expect(error).not.toHaveProperty("proof"); | |
| 189 | }).pipe(Effect.provide(layer(() => Effect.fail(cause)))); | |
| 182 | 190 | }); |
| 183 | 191 | |
| 184 | 192 | it.effect("accepts proofs bound to the access token hash", () => { |
infra/relay/src/environments/EnvironmentCredentials.test.ts+82| ... | ... | @@ -9,6 +9,88 @@ import { relayEnvironmentCredentials } from "../persistence/schema.ts"; |
| 9 | 9 | import * as EnvironmentCredentials from "./EnvironmentCredentials.ts"; |
| 10 | 10 | |
| 11 | 11 | describe("EnvironmentCredentials", () => { |
| 12 | it.effect("reports the credential creation persistence stage and preserves its cause", () => { | |
| 13 | const cause = new Error("database unavailable"); | |
| 14 | const fakeDb = { | |
| 15 | insert: (table: unknown) => { | |
| 16 | expect(table).toBe(relayEnvironmentCredentials); | |
| 17 | return { | |
| 18 | values: () => Effect.void, | |
| 19 | }; | |
| 20 | }, | |
| 21 | update: (table: unknown) => { | |
| 22 | expect(table).toBe(relayEnvironmentCredentials); | |
| 23 | return { | |
| 24 | set: () => ({ | |
| 25 | where: () => Effect.fail(cause), | |
| 26 | }), | |
| 27 | }; | |
| 28 | }, | |
| 29 | } as unknown as RelayDb.RelayDb["Service"]; | |
| 30 | ||
| 31 | return Effect.gen(function* () { | |
| 32 | const credentials = yield* EnvironmentCredentials.EnvironmentCredentials; | |
| 33 | const error = yield* Effect.flip( | |
| 34 | credentials.create({ | |
| 35 | environmentId: "env_test", | |
| 36 | environmentPublicKey: "sensitive-public-key-material", | |
| 37 | }), | |
| 38 | ); | |
| 39 | ||
| 40 | expect(error).toMatchObject({ | |
| 41 | _tag: "EnvironmentCredentialCreatePersistenceError", | |
| 42 | stage: "revoke-previous-credentials", | |
| 43 | environmentId: "env_test", | |
| 44 | }); | |
| 45 | expect(error.credentialId).toMatch(/^[0-9a-f]{64}$/); | |
| 46 | expect(error.cause).toBe(cause); | |
| 47 | expect(error).not.toHaveProperty("environmentPublicKey"); | |
| 48 | }).pipe( | |
| 49 | Effect.provide( | |
| 50 | EnvironmentCredentials.layer.pipe( | |
| 51 | Layer.provide(NodeCryptoLayer.layer), | |
| 52 | Layer.provide(Layer.succeed(RelayDb.RelayDb, fakeDb)), | |
| 53 | ), | |
| 54 | ), | |
| 55 | ); | |
| 56 | }); | |
| 57 | ||
| 58 | it.effect("does not retain credential tokens when lookup persistence fails", () => { | |
| 59 | const cause = new Error("database unavailable"); | |
| 60 | const token = "t3env_sensitive-credential-token"; | |
| 61 | const fakeDb = { | |
| 62 | select: () => ({ | |
| 63 | from: (table: unknown) => { | |
| 64 | expect(table).toBe(relayEnvironmentCredentials); | |
| 65 | return { | |
| 66 | where: () => ({ | |
| 67 | limit: () => Effect.fail(cause), | |
| 68 | }), | |
| 69 | }; | |
| 70 | }, | |
| 71 | }), | |
| 72 | } as unknown as RelayDb.RelayDb["Service"]; | |
| 73 | ||
| 74 | return Effect.gen(function* () { | |
| 75 | const credentials = yield* EnvironmentCredentials.EnvironmentCredentials; | |
| 76 | const error = yield* Effect.flip(credentials.authenticate(token)); | |
| 77 | ||
| 78 | expect(error).toMatchObject({ | |
| 79 | _tag: "EnvironmentCredentialAuthenticatePersistenceError", | |
| 80 | stage: "lookup-credential", | |
| 81 | }); | |
| 82 | expect(error.cause).toBe(cause); | |
| 83 | expect(error).not.toHaveProperty("token"); | |
| 84 | }).pipe( | |
| 85 | Effect.provide( | |
| 86 | EnvironmentCredentials.layer.pipe( | |
| 87 | Layer.provide(NodeCryptoLayer.layer), | |
| 88 | Layer.provide(Layer.succeed(RelayDb.RelayDb, fakeDb)), | |
| 89 | ), | |
| 90 | ), | |
| 91 | ); | |
| 92 | }); | |
| 93 | ||
| 12 | 94 | it.effect( |
| 13 | 95 | "creates opaque credentials and revokes only older credentials for the same key", |
| 14 | 96 | () => { |
infra/relay/src/environments/EnvironmentCredentials.ts+172-96| ... | ... | @@ -13,28 +13,44 @@ import { relayEnvironmentCredentials, relayEnvironmentLinks } from "../persisten |
| 13 | 13 | |
| 14 | 14 | export class EnvironmentCredentialCreatePersistenceError extends Schema.TaggedErrorClass<EnvironmentCredentialCreatePersistenceError>()( |
| 15 | 15 | "EnvironmentCredentialCreatePersistenceError", |
| 16 | { cause: Schema.Defect() }, | |
| 16 | { | |
| 17 | stage: Schema.Literals([ | |
| 18 | "generate-credential", | |
| 19 | "hash-token", | |
| 20 | "insert-credential", | |
| 21 | "revoke-previous-credentials", | |
| 22 | ]), | |
| 23 | environmentId: Schema.String, | |
| 24 | credentialId: Schema.optionalKey(Schema.String), | |
| 25 | cause: Schema.Defect(), | |
| 26 | }, | |
| 17 | 27 | ) { |
| 18 | 28 | override get message(): string { |
| 19 | return "Failed to persist environment credential"; | |
| 29 | return `Environment credential creation failed during '${this.stage}' for environment '${this.environmentId}'${this.credentialId === undefined ? "" : `, credential '${this.credentialId}'`}`; | |
| 20 | 30 | } |
| 21 | 31 | } |
| 22 | 32 | |
| 23 | 33 | export class EnvironmentCredentialAuthenticatePersistenceError extends Schema.TaggedErrorClass<EnvironmentCredentialAuthenticatePersistenceError>()( |
| 24 | 34 | "EnvironmentCredentialAuthenticatePersistenceError", |
| 25 | { cause: Schema.Defect() }, | |
| 35 | { | |
| 36 | stage: Schema.Literals(["hash-token", "lookup-credential"]), | |
| 37 | cause: Schema.Defect(), | |
| 38 | }, | |
| 26 | 39 | ) { |
| 27 | 40 | override get message(): string { |
| 28 | return "Failed to authenticate environment credential"; | |
| 41 | return `Environment credential authentication failed during '${this.stage}'`; | |
| 29 | 42 | } |
| 30 | 43 | } |
| 31 | 44 | |
| 32 | 45 | export class EnvironmentCredentialRevokePersistenceError extends Schema.TaggedErrorClass<EnvironmentCredentialRevokePersistenceError>()( |
| 33 | 46 | "EnvironmentCredentialRevokePersistenceError", |
| 34 | { cause: Schema.Defect() }, | |
| 47 | { | |
| 48 | environmentId: Schema.String, | |
| 49 | cause: Schema.Defect(), | |
| 50 | }, | |
| 35 | 51 | ) { |
| 36 | 52 | override get message(): string { |
| 37 | return "Failed to revoke environment credential"; | |
| 53 | return `Failed to revoke credentials for environment '${this.environmentId}'`; | |
| 38 | 54 | } |
| 39 | 55 | } |
| 40 | 56 | |
| ... | ... | @@ -85,13 +101,33 @@ const make = Effect.gen(function* () { |
| 85 | 101 | }); |
| 86 | 102 | |
| 87 | 103 | return EnvironmentCredentials.of({ |
| 88 | create: Effect.fn("relay.environment_credentials.create")( | |
| 89 | function* (input) { | |
| 90 | yield* Effect.annotateCurrentSpan({ "relay.environment_id": input.environmentId }); | |
| 91 | const credential = yield* makeCredential(); | |
| 92 | const credentialHash = yield* hashToken(credential.token); | |
| 93 | const now = DateTime.formatIso(yield* DateTime.now); | |
| 94 | yield* db.insert(relayEnvironmentCredentials).values({ | |
| 104 | create: Effect.fn("relay.environment_credentials.create")(function* (input) { | |
| 105 | yield* Effect.annotateCurrentSpan({ "relay.environment_id": input.environmentId }); | |
| 106 | const credential = yield* makeCredential().pipe( | |
| 107 | Effect.mapError( | |
| 108 | (cause) => | |
| 109 | new EnvironmentCredentialCreatePersistenceError({ | |
| 110 | stage: "generate-credential", | |
| 111 | environmentId: input.environmentId, | |
| 112 | cause, | |
| 113 | }), | |
| 114 | ), | |
| 115 | ); | |
| 116 | const credentialHash = yield* hashToken(credential.token).pipe( | |
| 117 | Effect.mapError( | |
| 118 | (cause) => | |
| 119 | new EnvironmentCredentialCreatePersistenceError({ | |
| 120 | stage: "hash-token", | |
| 121 | environmentId: input.environmentId, | |
| 122 | credentialId: credential.credentialId, | |
| 123 | cause, | |
| 124 | }), | |
| 125 | ), | |
| 126 | ); | |
| 127 | const now = DateTime.formatIso(yield* DateTime.now); | |
| 128 | yield* db | |
| 129 | .insert(relayEnvironmentCredentials) | |
| 130 | .values({ | |
| 95 | 131 | credentialId: credential.credentialId, |
| 96 | 132 | environmentId: input.environmentId, |
| 97 | 133 | environmentPublicKey: input.environmentPublicKey, |
| ... | ... | @@ -99,96 +135,136 @@ const make = Effect.gen(function* () { |
| 99 | 135 | revokedAt: null, |
| 100 | 136 | createdAt: now, |
| 101 | 137 | updatedAt: now, |
| 102 | }); | |
| 103 | yield* db | |
| 104 | .update(relayEnvironmentCredentials) | |
| 105 | .set({ | |
| 106 | revokedAt: now, | |
| 107 | updatedAt: now, | |
| 108 | }) | |
| 109 | .where( | |
| 110 | and( | |
| 111 | eq(relayEnvironmentCredentials.environmentId, input.environmentId), | |
| 112 | eq(relayEnvironmentCredentials.environmentPublicKey, input.environmentPublicKey), | |
| 113 | ne(relayEnvironmentCredentials.credentialId, credential.credentialId), | |
| 114 | isNull(relayEnvironmentCredentials.revokedAt), | |
| 115 | ), | |
| 116 | ); | |
| 117 | return credential.token; | |
| 118 | }, | |
| 119 | Effect.mapError((cause) => new EnvironmentCredentialCreatePersistenceError({ cause })), | |
| 120 | ), | |
| 138 | }) | |
| 139 | .pipe( | |
| 140 | Effect.mapError( | |
| 141 | (cause) => | |
| 142 | new EnvironmentCredentialCreatePersistenceError({ | |
| 143 | stage: "insert-credential", | |
| 144 | environmentId: input.environmentId, | |
| 145 | credentialId: credential.credentialId, | |
| 146 | cause, | |
| 147 | }), | |
| 148 | ), | |
| 149 | ); | |
| 150 | yield* db | |
| 151 | .update(relayEnvironmentCredentials) | |
| 152 | .set({ | |
| 153 | revokedAt: now, | |
| 154 | updatedAt: now, | |
| 155 | }) | |
| 156 | .where( | |
| 157 | and( | |
| 158 | eq(relayEnvironmentCredentials.environmentId, input.environmentId), | |
| 159 | eq(relayEnvironmentCredentials.environmentPublicKey, input.environmentPublicKey), | |
| 160 | ne(relayEnvironmentCredentials.credentialId, credential.credentialId), | |
| 161 | isNull(relayEnvironmentCredentials.revokedAt), | |
| 162 | ), | |
| 163 | ) | |
| 164 | .pipe( | |
| 165 | Effect.mapError( | |
| 166 | (cause) => | |
| 167 | new EnvironmentCredentialCreatePersistenceError({ | |
| 168 | stage: "revoke-previous-credentials", | |
| 169 | environmentId: input.environmentId, | |
| 170 | credentialId: credential.credentialId, | |
| 171 | cause, | |
| 172 | }), | |
| 173 | ), | |
| 174 | ); | |
| 175 | return credential.token; | |
| 176 | }), | |
| 121 | 177 | |
| 122 | authenticate: Effect.fn("relay.environment_credentials.authenticate")( | |
| 123 | function* (token) { | |
| 124 | const credentialHash = yield* hashToken(token); | |
| 125 | const rows = yield* db | |
| 126 | .select({ | |
| 127 | credentialId: relayEnvironmentCredentials.credentialId, | |
| 128 | environmentId: relayEnvironmentCredentials.environmentId, | |
| 129 | environmentPublicKey: relayEnvironmentCredentials.environmentPublicKey, | |
| 178 | authenticate: Effect.fn("relay.environment_credentials.authenticate")(function* (token) { | |
| 179 | const credentialHash = yield* hashToken(token).pipe( | |
| 180 | Effect.mapError( | |
| 181 | (cause) => | |
| 182 | new EnvironmentCredentialAuthenticatePersistenceError({ | |
| 183 | stage: "hash-token", | |
| 184 | cause, | |
| 185 | }), | |
| 186 | ), | |
| 187 | ); | |
| 188 | const rows = yield* db | |
| 189 | .select({ | |
| 190 | credentialId: relayEnvironmentCredentials.credentialId, | |
| 191 | environmentId: relayEnvironmentCredentials.environmentId, | |
| 192 | environmentPublicKey: relayEnvironmentCredentials.environmentPublicKey, | |
| 193 | }) | |
| 194 | .from(relayEnvironmentCredentials) | |
| 195 | .where( | |
| 196 | and( | |
| 197 | eq(relayEnvironmentCredentials.credentialHash, credentialHash), | |
| 198 | isNull(relayEnvironmentCredentials.revokedAt), | |
| 199 | ), | |
| 200 | ) | |
| 201 | .limit(1) | |
| 202 | .pipe( | |
| 203 | Effect.mapError( | |
| 204 | (cause) => | |
| 205 | new EnvironmentCredentialAuthenticatePersistenceError({ | |
| 206 | stage: "lookup-credential", | |
| 207 | cause, | |
| 208 | }), | |
| 209 | ), | |
| 210 | ); | |
| 211 | const row = rows[0]; | |
| 212 | if (row) { | |
| 213 | yield* Effect.annotateCurrentSpan({ "relay.environment_id": row.environmentId }); | |
| 214 | } | |
| 215 | return row | |
| 216 | ? Option.some({ | |
| 217 | credentialId: row.credentialId, | |
| 218 | environmentId: row.environmentId, | |
| 219 | environmentPublicKey: row.environmentPublicKey, | |
| 130 | 220 | }) |
| 131 | .from(relayEnvironmentCredentials) | |
| 132 | .where( | |
| 133 | and( | |
| 134 | eq(relayEnvironmentCredentials.credentialHash, credentialHash), | |
| 135 | isNull(relayEnvironmentCredentials.revokedAt), | |
| 136 | ), | |
| 137 | ) | |
| 138 | .limit(1); | |
| 139 | const row = rows[0]; | |
| 140 | if (row) { | |
| 141 | yield* Effect.annotateCurrentSpan({ "relay.environment_id": row.environmentId }); | |
| 142 | } | |
| 143 | return row | |
| 144 | ? Option.some({ | |
| 145 | credentialId: row.credentialId, | |
| 146 | environmentId: row.environmentId, | |
| 147 | environmentPublicKey: row.environmentPublicKey, | |
| 148 | }) | |
| 149 | : Option.none(); | |
| 150 | }, | |
| 151 | Effect.mapError((cause) => new EnvironmentCredentialAuthenticatePersistenceError({ cause })), | |
| 152 | ), | |
| 221 | : Option.none(); | |
| 222 | }), | |
| 153 | 223 | |
| 154 | 224 | revokeForEnvironmentPublicKey: Effect.fn( |
| 155 | 225 | "relay.environment_credentials.revoke_for_environment_public_key", |
| 156 | )( | |
| 157 | function* (input) { | |
| 158 | yield* Effect.annotateCurrentSpan({ "relay.environment_id": input.environmentId }); | |
| 159 | const revokedAt = DateTime.formatIso(yield* DateTime.now); | |
| 160 | const rows = yield* db | |
| 161 | .update(relayEnvironmentCredentials) | |
| 162 | .set({ | |
| 163 | revokedAt, | |
| 164 | updatedAt: revokedAt, | |
| 165 | }) | |
| 166 | .where( | |
| 167 | and( | |
| 168 | eq(relayEnvironmentCredentials.environmentId, input.environmentId), | |
| 169 | eq(relayEnvironmentCredentials.environmentPublicKey, input.environmentPublicKey), | |
| 170 | isNull(relayEnvironmentCredentials.revokedAt), | |
| 171 | notExists( | |
| 172 | db | |
| 173 | .select({ userId: relayEnvironmentLinks.userId }) | |
| 174 | .from(relayEnvironmentLinks) | |
| 175 | .where( | |
| 176 | and( | |
| 177 | eq(relayEnvironmentLinks.environmentId, input.environmentId), | |
| 178 | eq(relayEnvironmentLinks.environmentPublicKey, input.environmentPublicKey), | |
| 179 | isNull(relayEnvironmentLinks.revokedAt), | |
| 180 | ), | |
| 226 | )(function* (input) { | |
| 227 | yield* Effect.annotateCurrentSpan({ "relay.environment_id": input.environmentId }); | |
| 228 | const revokedAt = DateTime.formatIso(yield* DateTime.now); | |
| 229 | const rows = yield* db | |
| 230 | .update(relayEnvironmentCredentials) | |
| 231 | .set({ | |
| 232 | revokedAt, | |
| 233 | updatedAt: revokedAt, | |
| 234 | }) | |
| 235 | .where( | |
| 236 | and( | |
| 237 | eq(relayEnvironmentCredentials.environmentId, input.environmentId), | |
| 238 | eq(relayEnvironmentCredentials.environmentPublicKey, input.environmentPublicKey), | |
| 239 | isNull(relayEnvironmentCredentials.revokedAt), | |
| 240 | notExists( | |
| 241 | db | |
| 242 | .select({ userId: relayEnvironmentLinks.userId }) | |
| 243 | .from(relayEnvironmentLinks) | |
| 244 | .where( | |
| 245 | and( | |
| 246 | eq(relayEnvironmentLinks.environmentId, input.environmentId), | |
| 247 | eq(relayEnvironmentLinks.environmentPublicKey, input.environmentPublicKey), | |
| 248 | isNull(relayEnvironmentLinks.revokedAt), | |
| 181 | 249 | ), |
| 182 | ), | |
| 250 | ), | |
| 183 | 251 | ), |
| 184 | ) | |
| 185 | .returning({ | |
| 186 | credentialId: relayEnvironmentCredentials.credentialId, | |
| 187 | }); | |
| 188 | return rows.length > 0; | |
| 189 | }, | |
| 190 | Effect.mapError((cause) => new EnvironmentCredentialRevokePersistenceError({ cause })), | |
| 191 | ), | |
| 252 | ), | |
| 253 | ) | |
| 254 | .returning({ | |
| 255 | credentialId: relayEnvironmentCredentials.credentialId, | |
| 256 | }) | |
| 257 | .pipe( | |
| 258 | Effect.mapError( | |
| 259 | (cause) => | |
| 260 | new EnvironmentCredentialRevokePersistenceError({ | |
| 261 | environmentId: input.environmentId, | |
| 262 | cause, | |
| 263 | }), | |
| 264 | ), | |
| 265 | ); | |
| 266 | return rows.length > 0; | |
| 267 | }), | |
| 192 | 268 | }); |
| 193 | 269 | }); |
| 194 | 270 |
infra/relay/src/environments/EnvironmentLinks.test.ts+70| ... | ... | @@ -8,6 +8,76 @@ import { relayEnvironmentLinks } from "../persistence/schema.ts"; |
| 8 | 8 | import * as EnvironmentLinks from "./EnvironmentLinks.ts"; |
| 9 | 9 | |
| 10 | 10 | describe("EnvironmentLinks", () => { |
| 11 | it.effect("retains link lookup failures with user and environment identity", () => { | |
| 12 | const cause = new Error("database unavailable"); | |
| 13 | const fakeDb = { | |
| 14 | select: () => ({ | |
| 15 | from: (table: unknown) => { | |
| 16 | expect(table).toBe(relayEnvironmentLinks); | |
| 17 | return { | |
| 18 | where: () => ({ | |
| 19 | limit: () => Effect.fail(cause), | |
| 20 | }), | |
| 21 | }; | |
| 22 | }, | |
| 23 | }), | |
| 24 | } as unknown as RelayDb.RelayDb["Service"]; | |
| 25 | ||
| 26 | return Effect.gen(function* () { | |
| 27 | const links = yield* EnvironmentLinks.EnvironmentLinks; | |
| 28 | const error = yield* Effect.flip( | |
| 29 | links.getForUser({ userId: "user-1", environmentId: "env-1" }), | |
| 30 | ); | |
| 31 | ||
| 32 | expect(error).toMatchObject({ | |
| 33 | _tag: "EnvironmentLinkLookupPersistenceError", | |
| 34 | userId: "user-1", | |
| 35 | environmentId: "env-1", | |
| 36 | }); | |
| 37 | expect(error.cause).toBe(cause); | |
| 38 | }).pipe( | |
| 39 | Effect.provide( | |
| 40 | EnvironmentLinks.layer.pipe(Layer.provide(Layer.succeed(RelayDb.RelayDb, fakeDb))), | |
| 41 | ), | |
| 42 | ); | |
| 43 | }); | |
| 44 | ||
| 45 | it.effect("identifies delivery-user list failures without retaining key material", () => { | |
| 46 | const cause = new Error("database unavailable"); | |
| 47 | const fakeDb = { | |
| 48 | select: () => ({ | |
| 49 | from: (table: unknown) => { | |
| 50 | expect(table).toBe(relayEnvironmentLinks); | |
| 51 | return { | |
| 52 | where: () => Effect.fail(cause), | |
| 53 | }; | |
| 54 | }, | |
| 55 | }), | |
| 56 | } as unknown as RelayDb.RelayDb["Service"]; | |
| 57 | ||
| 58 | return Effect.gen(function* () { | |
| 59 | const links = yield* EnvironmentLinks.EnvironmentLinks; | |
| 60 | const error = yield* Effect.flip( | |
| 61 | links.listDeliveryUsersForEnvironment({ | |
| 62 | environmentId: "env-1", | |
| 63 | environmentPublicKey: "sensitive-public-key-material", | |
| 64 | }), | |
| 65 | ); | |
| 66 | ||
| 67 | expect(error).toMatchObject({ | |
| 68 | _tag: "EnvironmentLinkUserListPersistenceError", | |
| 69 | operation: "list-delivery-users", | |
| 70 | environmentId: "env-1", | |
| 71 | }); | |
| 72 | expect(error.cause).toBe(cause); | |
| 73 | expect(error).not.toHaveProperty("environmentPublicKey"); | |
| 74 | }).pipe( | |
| 75 | Effect.provide( | |
| 76 | EnvironmentLinks.layer.pipe(Layer.provide(Layer.succeed(RelayDb.RelayDb, fakeDb))), | |
| 77 | ), | |
| 78 | ); | |
| 79 | }); | |
| 80 | ||
| 11 | 81 | it.effect("selects users when either notifications or Live Activities are enabled", () => { |
| 12 | 82 | const whereConditions: Array<unknown> = []; |
| 13 | 83 | const fakeDb = { |
infra/relay/src/environments/EnvironmentLinks.ts+148-77| ... | ... | @@ -26,55 +26,78 @@ export interface AgentAwarenessDeliveryUserRecord { |
| 26 | 26 | |
| 27 | 27 | export class EnvironmentLinkUpsertPersistenceError extends Schema.TaggedErrorClass<EnvironmentLinkUpsertPersistenceError>()( |
| 28 | 28 | "EnvironmentLinkUpsertPersistenceError", |
| 29 | { cause: Schema.Defect() }, | |
| 29 | { | |
| 30 | userId: Schema.String, | |
| 31 | environmentId: Schema.String, | |
| 32 | deviceId: Schema.optionalKey(Schema.String), | |
| 33 | cause: Schema.Defect(), | |
| 34 | }, | |
| 30 | 35 | ) { |
| 31 | 36 | override get message(): string { |
| 32 | return "Failed to persist environment link"; | |
| 37 | return `Failed to persist environment link for user '${this.userId}', environment '${this.environmentId}'`; | |
| 33 | 38 | } |
| 34 | 39 | } |
| 35 | 40 | |
| 36 | 41 | export class EnvironmentLinkUserListPersistenceError extends Schema.TaggedErrorClass<EnvironmentLinkUserListPersistenceError>()( |
| 37 | 42 | "EnvironmentLinkUserListPersistenceError", |
| 38 | { cause: Schema.Defect() }, | |
| 43 | { | |
| 44 | operation: Schema.Literals(["list-users", "list-delivery-users"]), | |
| 45 | environmentId: Schema.String, | |
| 46 | cause: Schema.Defect(), | |
| 47 | }, | |
| 39 | 48 | ) { |
| 40 | 49 | override get message(): string { |
| 41 | return "Failed to list users linked to environment"; | |
| 50 | return `Environment link user query '${this.operation}' failed for environment '${this.environmentId}'`; | |
| 42 | 51 | } |
| 43 | 52 | } |
| 44 | 53 | |
| 45 | 54 | export class EnvironmentPublicKeyListPersistenceError extends Schema.TaggedErrorClass<EnvironmentPublicKeyListPersistenceError>()( |
| 46 | 55 | "EnvironmentPublicKeyListPersistenceError", |
| 47 | { cause: Schema.Defect() }, | |
| 56 | { | |
| 57 | environmentId: Schema.String, | |
| 58 | cause: Schema.Defect(), | |
| 59 | }, | |
| 48 | 60 | ) { |
| 49 | 61 | override get message(): string { |
| 50 | return "Failed to list environment public keys"; | |
| 62 | return `Failed to list public keys for environment '${this.environmentId}'`; | |
| 51 | 63 | } |
| 52 | 64 | } |
| 53 | 65 | |
| 54 | 66 | export class EnvironmentLinkListPersistenceError extends Schema.TaggedErrorClass<EnvironmentLinkListPersistenceError>()( |
| 55 | 67 | "EnvironmentLinkListPersistenceError", |
| 56 | { cause: Schema.Defect() }, | |
| 68 | { | |
| 69 | userId: Schema.String, | |
| 70 | cause: Schema.Defect(), | |
| 71 | }, | |
| 57 | 72 | ) { |
| 58 | 73 | override get message(): string { |
| 59 | return "Failed to list environment links"; | |
| 74 | return `Failed to list environment links for user '${this.userId}'`; | |
| 60 | 75 | } |
| 61 | 76 | } |
| 62 | 77 | |
| 63 | 78 | export class EnvironmentLinkLookupPersistenceError extends Schema.TaggedErrorClass<EnvironmentLinkLookupPersistenceError>()( |
| 64 | 79 | "EnvironmentLinkLookupPersistenceError", |
| 65 | { cause: Schema.Defect() }, | |
| 80 | { | |
| 81 | userId: Schema.String, | |
| 82 | environmentId: Schema.String, | |
| 83 | cause: Schema.Defect(), | |
| 84 | }, | |
| 66 | 85 | ) { |
| 67 | 86 | override get message(): string { |
| 68 | return "Failed to look up environment link"; | |
| 87 | return `Failed to look up environment link for user '${this.userId}', environment '${this.environmentId}'`; | |
| 69 | 88 | } |
| 70 | 89 | } |
| 71 | 90 | |
| 72 | 91 | export class EnvironmentLinkRevokePersistenceError extends Schema.TaggedErrorClass<EnvironmentLinkRevokePersistenceError>()( |
| 73 | 92 | "EnvironmentLinkRevokePersistenceError", |
| 74 | { cause: Schema.Defect() }, | |
| 93 | { | |
| 94 | userId: Schema.String, | |
| 95 | environmentId: Schema.String, | |
| 96 | cause: Schema.Defect(), | |
| 97 | }, | |
| 75 | 98 | ) { |
| 76 | 99 | override get message(): string { |
| 77 | return "Failed to revoke environment link"; | |
| 100 | return `Failed to revoke environment link for user '${this.userId}', environment '${this.environmentId}'`; | |
| 78 | 101 | } |
| 79 | 102 | } |
| 80 | 103 | |
| ... | ... | @@ -142,22 +165,37 @@ const make = Effect.gen(function* () { |
| 142 | 165 | const db = yield* RelayDb.RelayDb; |
| 143 | 166 | |
| 144 | 167 | return EnvironmentLinks.of({ |
| 145 | upsert: Effect.fn("relay.environment_links.upsert")( | |
| 146 | function* (input) { | |
| 147 | yield* Effect.annotateCurrentSpan({ | |
| 148 | "relay.environment_id": input.proof.environmentId, | |
| 149 | }); | |
| 150 | const now = DateTime.formatIso(yield* DateTime.now); | |
| 151 | const { request, proof } = input; | |
| 152 | const environmentId = proof.environmentId; | |
| 153 | const { endpoint } = input; | |
| 154 | yield* db | |
| 155 | .insert(relayEnvironmentLinks) | |
| 156 | .values({ | |
| 157 | userId: input.userId, | |
| 158 | environmentId, | |
| 159 | environmentLabel: proof.descriptor.label, | |
| 168 | upsert: Effect.fn("relay.environment_links.upsert")(function* (input) { | |
| 169 | yield* Effect.annotateCurrentSpan({ | |
| 170 | "relay.environment_id": input.proof.environmentId, | |
| 171 | }); | |
| 172 | const now = DateTime.formatIso(yield* DateTime.now); | |
| 173 | const { request, proof } = input; | |
| 174 | const environmentId = proof.environmentId; | |
| 175 | const { endpoint } = input; | |
| 176 | yield* db | |
| 177 | .insert(relayEnvironmentLinks) | |
| 178 | .values({ | |
| 179 | userId: input.userId, | |
| 180 | environmentId, | |
| 181 | environmentLabel: proof.descriptor.label, | |
| 182 | environmentPublicKey: proof.environmentPublicKey, | |
| 183 | endpointHttpBaseUrl: endpoint.httpBaseUrl, | |
| 184 | endpointWsBaseUrl: endpoint.wsBaseUrl, | |
| 185 | endpointProviderKind: endpoint.providerKind, | |
| 186 | notificationsEnabled: request.notificationsEnabled, | |
| 187 | liveActivitiesEnabled: request.liveActivitiesEnabled, | |
| 188 | managedTunnelsEnabled: request.managedTunnelsEnabled, | |
| 189 | createdByDeviceId: request.deviceId ?? null, | |
| 190 | revokedAt: null, | |
| 191 | createdAt: now, | |
| 192 | updatedAt: now, | |
| 193 | }) | |
| 194 | .onConflictDoUpdate({ | |
| 195 | target: [relayEnvironmentLinks.userId, relayEnvironmentLinks.environmentId], | |
| 196 | set: { | |
| 160 | 197 | environmentPublicKey: proof.environmentPublicKey, |
| 198 | environmentLabel: proof.descriptor.label, | |
| 161 | 199 | endpointHttpBaseUrl: endpoint.httpBaseUrl, |
| 162 | 200 | endpointWsBaseUrl: endpoint.wsBaseUrl, |
| 163 | 201 | endpointProviderKind: endpoint.providerKind, |
| ... | ... | @@ -166,28 +204,21 @@ const make = Effect.gen(function* () { |
| 166 | 204 | managedTunnelsEnabled: request.managedTunnelsEnabled, |
| 167 | 205 | createdByDeviceId: request.deviceId ?? null, |
| 168 | 206 | revokedAt: null, |
| 169 | createdAt: now, | |
| 170 | 207 | updatedAt: now, |
| 171 | }) | |
| 172 | .onConflictDoUpdate({ | |
| 173 | target: [relayEnvironmentLinks.userId, relayEnvironmentLinks.environmentId], | |
| 174 | set: { | |
| 175 | environmentPublicKey: proof.environmentPublicKey, | |
| 176 | environmentLabel: proof.descriptor.label, | |
| 177 | endpointHttpBaseUrl: endpoint.httpBaseUrl, | |
| 178 | endpointWsBaseUrl: endpoint.wsBaseUrl, | |
| 179 | endpointProviderKind: endpoint.providerKind, | |
| 180 | notificationsEnabled: request.notificationsEnabled, | |
| 181 | liveActivitiesEnabled: request.liveActivitiesEnabled, | |
| 182 | managedTunnelsEnabled: request.managedTunnelsEnabled, | |
| 183 | createdByDeviceId: request.deviceId ?? null, | |
| 184 | revokedAt: null, | |
| 185 | updatedAt: now, | |
| 186 | }, | |
| 187 | }); | |
| 188 | }, | |
| 189 | Effect.mapError((cause) => new EnvironmentLinkUpsertPersistenceError({ cause })), | |
| 190 | ), | |
| 208 | }, | |
| 209 | }) | |
| 210 | .pipe( | |
| 211 | Effect.mapError( | |
| 212 | (cause) => | |
| 213 | new EnvironmentLinkUpsertPersistenceError({ | |
| 214 | userId: input.userId, | |
| 215 | environmentId, | |
| 216 | ...(request.deviceId === undefined ? {} : { deviceId: request.deviceId }), | |
| 217 | cause, | |
| 218 | }), | |
| 219 | ), | |
| 220 | ); | |
| 221 | }), | |
| 191 | 222 | |
| 192 | 223 | listUsersForEnvironment: Effect.fn("relay.environment_links.list_users_for_environment")( |
| 193 | 224 | function* (input) { |
| ... | ... | @@ -198,7 +229,14 @@ const make = Effect.gen(function* () { |
| 198 | 229 | .where(agentAwarenessDeliveryUserCondition(input.environmentId)) |
| 199 | 230 | .pipe( |
| 200 | 231 | Effect.map((rows) => rows.map((row) => row.userId)), |
| 201 | Effect.mapError((cause) => new EnvironmentLinkUserListPersistenceError({ cause })), | |
| 232 | Effect.mapError( | |
| 233 | (cause) => | |
| 234 | new EnvironmentLinkUserListPersistenceError({ | |
| 235 | operation: "list-users", | |
| 236 | environmentId: input.environmentId, | |
| 237 | cause, | |
| 238 | }), | |
| 239 | ), | |
| 202 | 240 | ); |
| 203 | 241 | }, |
| 204 | 242 | ), |
| ... | ... | @@ -223,7 +261,14 @@ const make = Effect.gen(function* () { |
| 223 | 261 | liveActivitiesEnabled: row.liveActivitiesEnabled, |
| 224 | 262 | })), |
| 225 | 263 | ), |
| 226 | Effect.mapError((cause) => new EnvironmentLinkUserListPersistenceError({ cause })), | |
| 264 | Effect.mapError( | |
| 265 | (cause) => | |
| 266 | new EnvironmentLinkUserListPersistenceError({ | |
| 267 | operation: "list-delivery-users", | |
| 268 | environmentId: input.environmentId, | |
| 269 | cause, | |
| 270 | }), | |
| 271 | ), | |
| 227 | 272 | ); |
| 228 | 273 | }), |
| 229 | 274 | |
| ... | ... | @@ -244,7 +289,13 @@ const make = Effect.gen(function* () { |
| 244 | 289 | Effect.map((rows) => [ |
| 245 | 290 | ...new Set(rows.map((row) => row.environmentPublicKey).filter((key) => key.length > 0)), |
| 246 | 291 | ]), |
| 247 | Effect.mapError((cause) => new EnvironmentPublicKeyListPersistenceError({ cause })), | |
| 292 | Effect.mapError( | |
| 293 | (cause) => | |
| 294 | new EnvironmentPublicKeyListPersistenceError({ | |
| 295 | environmentId: input.environmentId, | |
| 296 | cause, | |
| 297 | }), | |
| 298 | ), | |
| 248 | 299 | ); |
| 249 | 300 | }), |
| 250 | 301 | |
| ... | ... | @@ -280,7 +331,13 @@ const make = Effect.gen(function* () { |
| 280 | 331 | linkedAt: row.createdAt, |
| 281 | 332 | })), |
| 282 | 333 | ), |
| 283 | Effect.mapError((cause) => new EnvironmentLinkListPersistenceError({ cause })), | |
| 334 | Effect.mapError( | |
| 335 | (cause) => | |
| 336 | new EnvironmentLinkListPersistenceError({ | |
| 337 | userId: input.userId, | |
| 338 | cause, | |
| 339 | }), | |
| 340 | ), | |
| 284 | 341 | ); |
| 285 | 342 | }), |
| 286 | 343 | |
| ... | ... | @@ -328,34 +385,48 @@ const make = Effect.gen(function* () { |
| 328 | 385 | } |
| 329 | 386 | : null; |
| 330 | 387 | }), |
| 331 | Effect.mapError((cause) => new EnvironmentLinkLookupPersistenceError({ cause })), | |
| 388 | Effect.mapError( | |
| 389 | (cause) => | |
| 390 | new EnvironmentLinkLookupPersistenceError({ | |
| 391 | userId: input.userId, | |
| 392 | environmentId: input.environmentId, | |
| 393 | cause, | |
| 394 | }), | |
| 395 | ), | |
| 332 | 396 | ); |
| 333 | 397 | }), |
| 334 | 398 | |
| 335 | revokeForUser: Effect.fn("relay.environment_links.revoke_for_user")( | |
| 336 | function* (input) { | |
| 337 | yield* Effect.annotateCurrentSpan({ | |
| 338 | "relay.environment_id": input.environmentId, | |
| 339 | }); | |
| 340 | const revokedAt = DateTime.formatIso(yield* DateTime.now); | |
| 341 | const rows = yield* db | |
| 342 | .update(relayEnvironmentLinks) | |
| 343 | .set({ | |
| 344 | revokedAt, | |
| 345 | updatedAt: revokedAt, | |
| 346 | }) | |
| 347 | .where( | |
| 348 | and( | |
| 349 | eq(relayEnvironmentLinks.userId, input.userId), | |
| 350 | eq(relayEnvironmentLinks.environmentId, input.environmentId), | |
| 351 | isNull(relayEnvironmentLinks.revokedAt), | |
| 352 | ), | |
| 353 | ) | |
| 354 | .returning({ environmentId: relayEnvironmentLinks.environmentId }); | |
| 355 | return rows.length > 0; | |
| 356 | }, | |
| 357 | Effect.mapError((cause) => new EnvironmentLinkRevokePersistenceError({ cause })), | |
| 358 | ), | |
| 399 | revokeForUser: Effect.fn("relay.environment_links.revoke_for_user")(function* (input) { | |
| 400 | yield* Effect.annotateCurrentSpan({ | |
| 401 | "relay.environment_id": input.environmentId, | |
| 402 | }); | |
| 403 | const revokedAt = DateTime.formatIso(yield* DateTime.now); | |
| 404 | const rows = yield* db | |
| 405 | .update(relayEnvironmentLinks) | |
| 406 | .set({ | |
| 407 | revokedAt, | |
| 408 | updatedAt: revokedAt, | |
| 409 | }) | |
| 410 | .where( | |
| 411 | and( | |
| 412 | eq(relayEnvironmentLinks.userId, input.userId), | |
| 413 | eq(relayEnvironmentLinks.environmentId, input.environmentId), | |
| 414 | isNull(relayEnvironmentLinks.revokedAt), | |
| 415 | ), | |
| 416 | ) | |
| 417 | .returning({ environmentId: relayEnvironmentLinks.environmentId }) | |
| 418 | .pipe( | |
| 419 | Effect.mapError( | |
| 420 | (cause) => | |
| 421 | new EnvironmentLinkRevokePersistenceError({ | |
| 422 | userId: input.userId, | |
| 423 | environmentId: input.environmentId, | |
| 424 | cause, | |
| 425 | }), | |
| 426 | ), | |
| 427 | ); | |
| 428 | return rows.length > 0; | |
| 429 | }), | |
| 359 | 430 | }); |
| 360 | 431 | }); |
| 361 | 432 |
infra/relay/src/http/Api.test.ts+1| ... | ... | @@ -108,6 +108,7 @@ describe("relay client authentication", () => { |
| 108 | 108 | describe("relay environment authentication", () => { |
| 109 | 109 | it.effect("preserves credential lookup persistence failures as internal errors", () => { |
| 110 | 110 | const failure = new EnvironmentCredentials.EnvironmentCredentialAuthenticatePersistenceError({ |
| 111 | stage: "lookup-credential", | |
| 111 | 112 | cause: "database unavailable", |
| 112 | 113 | }); |
| 113 | 114 | const credentials: EnvironmentCredentials.EnvironmentCredentials["Service"] = { |
infra/relay/src/http/Api.ts+71-25| ... | ... | @@ -238,13 +238,12 @@ export const relayEnvironmentAuthLayer = Layer.effect( |
| 238 | 238 | { credential }, |
| 239 | 239 | ) { |
| 240 | 240 | const token = readHttpAuthorizationCredential(credential); |
| 241 | const principal = yield* credentials | |
| 242 | .authenticate(token) | |
| 243 | .pipe( | |
| 244 | Effect.catchTag("EnvironmentCredentialAuthenticatePersistenceError", () => | |
| 241 | const principal = yield* credentials.authenticate(token).pipe( | |
| 242 | Effect.catchTags({ | |
| 243 | EnvironmentCredentialAuthenticatePersistenceError: () => | |
| 245 | 244 | relayInternalErrorResponse("persistence_failed"), |
| 246 | ), | |
| 247 | ); | |
| 245 | }), | |
| 246 | ); | |
| 248 | 247 | if (principal._tag === "None") { |
| 249 | 248 | return yield* relayAuthInvalidError("not_authorized"); |
| 250 | 249 | } |
| ... | ... | @@ -777,17 +776,72 @@ export const serverApi = HttpApiBuilder.group( |
| 777 | 776 | reason: "persistence_failed", |
| 778 | 777 | traceId, |
| 779 | 778 | }), |
| 780 | ApnsDeliveryJobQueuePayloadInvalid: mapApnsDeliveryJobInternalError, | |
| 781 | ApnsDeliveryJobLiveActivityAggregateMissing: mapApnsDeliveryJobInternalError, | |
| 782 | ApnsDeliveryJobLiveActivityNotificationUnexpected: mapApnsDeliveryJobInternalError, | |
| 783 | ApnsDeliveryJobPushNotificationMissing: mapApnsDeliveryJobInternalError, | |
| 784 | ApnsDeliveryJobPushNotificationAggregateUnexpected: mapApnsDeliveryJobInternalError, | |
| 785 | ApnsDeliveryJobCreatedAtInvalid: mapApnsDeliveryJobInternalError, | |
| 786 | ApnsDeliveryJobExpiresAtInvalid: mapApnsDeliveryJobInternalError, | |
| 787 | ApnsDeliveryJobTimeWindowInvalid: mapApnsDeliveryJobInternalError, | |
| 788 | ApnsDeliveryJobTimeWindowTooLong: mapApnsDeliveryJobInternalError, | |
| 789 | ApnsDeliveryJobSignatureInvalid: mapApnsDeliveryJobInternalError, | |
| 790 | ApnsDeliveryJobExpired: mapApnsDeliveryJobInternalError, | |
| 779 | ApnsDeliveryJobQueuePayloadInvalid: (_error, traceId) => | |
| 780 | new RelayInternalError({ | |
| 781 | code: "internal_error", | |
| 782 | reason: "internal_error", | |
| 783 | traceId, | |
| 784 | }), | |
| 785 | ApnsDeliveryJobLiveActivityAggregateMissing: (_error, traceId) => | |
| 786 | new RelayInternalError({ | |
| 787 | code: "internal_error", | |
| 788 | reason: "internal_error", | |
| 789 | traceId, | |
| 790 | }), | |
| 791 | ApnsDeliveryJobLiveActivityNotificationUnexpected: (_error, traceId) => | |
| 792 | new RelayInternalError({ | |
| 793 | code: "internal_error", | |
| 794 | reason: "internal_error", | |
| 795 | traceId, | |
| 796 | }), | |
| 797 | ApnsDeliveryJobPushNotificationMissing: (_error, traceId) => | |
| 798 | new RelayInternalError({ | |
| 799 | code: "internal_error", | |
| 800 | reason: "internal_error", | |
| 801 | traceId, | |
| 802 | }), | |
| 803 | ApnsDeliveryJobPushNotificationAggregateUnexpected: (_error, traceId) => | |
| 804 | new RelayInternalError({ | |
| 805 | code: "internal_error", | |
| 806 | reason: "internal_error", | |
| 807 | traceId, | |
| 808 | }), | |
| 809 | ApnsDeliveryJobCreatedAtInvalid: (_error, traceId) => | |
| 810 | new RelayInternalError({ | |
| 811 | code: "internal_error", | |
| 812 | reason: "internal_error", | |
| 813 | traceId, | |
| 814 | }), | |
| 815 | ApnsDeliveryJobExpiresAtInvalid: (_error, traceId) => | |
| 816 | new RelayInternalError({ | |
| 817 | code: "internal_error", | |
| 818 | reason: "internal_error", | |
| 819 | traceId, | |
| 820 | }), | |
| 821 | ApnsDeliveryJobTimeWindowInvalid: (_error, traceId) => | |
| 822 | new RelayInternalError({ | |
| 823 | code: "internal_error", | |
| 824 | reason: "internal_error", | |
| 825 | traceId, | |
| 826 | }), | |
| 827 | ApnsDeliveryJobTimeWindowTooLong: (_error, traceId) => | |
| 828 | new RelayInternalError({ | |
| 829 | code: "internal_error", | |
| 830 | reason: "internal_error", | |
| 831 | traceId, | |
| 832 | }), | |
| 833 | ApnsDeliveryJobSignatureInvalid: (_error, traceId) => | |
| 834 | new RelayInternalError({ | |
| 835 | code: "internal_error", | |
| 836 | reason: "internal_error", | |
| 837 | traceId, | |
| 838 | }), | |
| 839 | ApnsDeliveryJobExpired: (_error, traceId) => | |
| 840 | new RelayInternalError({ | |
| 841 | code: "internal_error", | |
| 842 | reason: "internal_error", | |
| 843 | traceId, | |
| 844 | }), | |
| 791 | 845 | ApnsDeliveryJobClaimInFlight: (_error, traceId) => |
| 792 | 846 | new RelayInternalError({ |
| 793 | 847 | code: "internal_error", |
| ... | ... | @@ -892,14 +946,6 @@ function mapRelayCommonApiErrors(authReason: RelayAuthInvalidReason) { |
| 892 | 946 | ): Effect.Effect<A, MapRelayCommonApiError<E>, R> => effect.pipe(Effect.catch(mapError)); |
| 893 | 947 | } |
| 894 | 948 | |
| 895 | function mapApnsDeliveryJobInternalError(_error: unknown, traceId: string) { | |
| 896 | return new RelayInternalError({ | |
| 897 | code: "internal_error", | |
| 898 | reason: "internal_error", | |
| 899 | traceId, | |
| 900 | }); | |
| 901 | } | |
| 902 | ||
| 903 | 949 | type TaggedErrorTag<E> = Extract<E, { readonly _tag: string }>["_tag"]; |
| 904 | 950 | |
| 905 | 951 | type MapErrorTagCases<E> = { |