@mccaskie-labs/workbench-supabase 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # @mccaskie-labs/workbench-supabase
2
2
 
3
+ ## 0.2.0
4
+
5
+ - `plainError(error, { appName })`: raw Postgres and PostgREST wording (permission denied, row-level security, missing relation, PGRST codes and so on) becomes one plain line naming the app, and the full message is logged. The app's own words pass unchanged. From Imprint's `plain-error.ts` (sign-in kit, 11 November 2026).
6
+ - `plainAuthError(error, { appName })`: a plain line for each common Supabase Auth error code. None says whether an account exists for an address.
7
+
3
8
  ## 0.1.0
4
9
 
5
10
  - First version (step 4.11): `supabaseConfig`, `createBrowserClient`, `createServerClient`, `createUserClient`, `createServiceClient` (logs its reason once per isolate), `WorkbenchSupabaseError`, and the CLI's `check:migrations`, `check:prod-schema` and `prod-schema-sql` (`docs/spec.md` §3).
package/README.md CHANGED
@@ -22,6 +22,8 @@ const admin = createServiceClient({ url: cfg.url, secretKey: workerSecret(env, '
22
22
  | `createServerClient({ ...cfg, cookies })` | On the server, as the signed-in user from the request's cookies. `cookies` is `{ getAll, setAll }` |
23
23
  | `createUserClient({ ...cfg, accessToken })` | As the caller, with the token they sent (audit F7). No session is stored or refreshed |
24
24
  | `createServiceClient({ url, secretKey, reason })` | The `sb_secret_` key: **bypasses RLS**. `reason` (one line) is logged once per isolate: `[workbench-supabase] service client (bypasses RLS): <reason>`. Refused in a browser and for a publishable key |
25
+ | `plainError(error, { appName, log? })` | The message to show a person: the app's own words unchanged, or for raw Postgres or PostgREST wording ("permission denied for function …", RLS, missing relation, `PGRST116`) one plain line, "Something went wrong on <app>'s side …". The full message is logged |
26
+ | `plainAuthError(error, { appName, log? })` | A plain line for a Supabase Auth error by its `code` (`otp_expired`, `invalid_credentials`, rate limits, `weak_password` …). None says whether an account exists; an unknown code gets the plain line and is logged |
25
27
  | `WorkbenchSupabaseError` | A client built wrongly: `code` is `invalid_config` or `forbidden`. Never holds a key, URL or token |
26
28
 
27
29
  Every client takes an optional `fetch`, for tests against a fake Supabase.
package/dist/cli.js CHANGED
File without changes
package/dist/index.d.ts CHANGED
@@ -5,3 +5,4 @@ export { migrationProblems, type MigrationProblem } from "./migrations.ts";
5
5
  export { PRODUCTION_SCHEMA_SQL, createdBy, missingOnProduction, findUses, unusedAllows, parseProductionSchema, parseAllow, type ProductionSchema, type AllowEntry, type MissingName, type Use, } from "./prod-schema.ts";
6
6
  export { stringPieces, type StringPiece } from "./strings.ts";
7
7
  export type { SupabaseClient } from "@supabase/supabase-js";
8
+ export { plainError, plainAuthError, plainServerError, isRawDbError, type PlainErrorOptions } from "./plain-error.ts";
package/dist/index.js CHANGED
@@ -4,3 +4,4 @@ export { WorkbenchSupabaseError } from "./errors.js";
4
4
  export { migrationProblems } from "./migrations.js";
5
5
  export { PRODUCTION_SCHEMA_SQL, createdBy, missingOnProduction, findUses, unusedAllows, parseProductionSchema, parseAllow, } from "./prod-schema.js";
6
6
  export { stringPieces } from "./strings.js";
7
+ export { plainError, plainAuthError, plainServerError, isRawDbError } from "./plain-error.js";
@@ -0,0 +1,21 @@
1
+ export interface PlainErrorOptions {
2
+ /** The app's name as people see it (brand.json `name`). */
3
+ appName: string;
4
+ /** Where the full message is logged. Defaults to console.error. */
5
+ log?: (line: string) => void;
6
+ }
7
+ /** True for wording from Postgres or PostgREST that a person shouldn't see. */
8
+ export declare function isRawDbError(message: string): boolean;
9
+ /** The line shown in place of a raw error. */
10
+ export declare function plainServerError(appName: string): string;
11
+ /**
12
+ * The message to show: as it is, or the plain line for a raw database error
13
+ * (logged in full). Takes a string, an Error or a Supabase/PostgREST error
14
+ * object; anything without a message is treated as raw.
15
+ */
16
+ export declare function plainError(error: unknown, opts: PlainErrorOptions): string;
17
+ /**
18
+ * A plain line for a Supabase Auth error, by its code. An unknown code (or
19
+ * none) gets the app's plain server line, and the error is logged in full.
20
+ */
21
+ export declare function plainAuthError(error: unknown, opts: PlainErrorOptions): string;
@@ -0,0 +1,76 @@
1
+ // Raw database and Auth errors never reach a person's screen. Carried across
2
+ // from Imprint's apps/admin/lib/plain-error.ts (sign-in kit, agreed
3
+ // 11 November 2026): a technical message is logged in full for us and
4
+ // replaced with one plain line naming the app. The app's own refusals,
5
+ // including its database functions' own words, pass unchanged.
6
+ /** Postgres and PostgREST wording that only makes sense to a developer. */
7
+ const RAW_DB = /permission denied for|violates (row-level security|foreign key|check|not-null|unique|exclusion)|duplicate key value|(relation|column|function|schema|type) "?[\w.]+(\([^)]*\))?"? does not exist|invalid input (syntax|value) for|syntax error at or near|could not serialize access|deadlock detected|null value in column|value too long for type|\bPGRST\d{3}\b|\bJWT\b|42501|23505|23503|42883|42P01/i;
8
+ /** True for wording from Postgres or PostgREST that a person shouldn't see. */
9
+ export function isRawDbError(message) {
10
+ return RAW_DB.test(message);
11
+ }
12
+ /** The line shown in place of a raw error. */
13
+ export function plainServerError(appName) {
14
+ return `Something went wrong on ${appName}’s side. Try again, and if it happens again, tell ${appName} support.`;
15
+ }
16
+ function messageOf(error) {
17
+ if (typeof error === "string")
18
+ return error;
19
+ if (error && typeof error === "object" && typeof error.message === "string") {
20
+ return error.message;
21
+ }
22
+ return "";
23
+ }
24
+ /**
25
+ * The message to show: as it is, or the plain line for a raw database error
26
+ * (logged in full). Takes a string, an Error or a Supabase/PostgREST error
27
+ * object; anything without a message is treated as raw.
28
+ */
29
+ export function plainError(error, opts) {
30
+ const message = messageOf(error);
31
+ if (message !== "" && !isRawDbError(message))
32
+ return message;
33
+ (opts.log ?? console.error)(`[error shown as plain] ${message || String(error)}`);
34
+ return plainServerError(opts.appName);
35
+ }
36
+ /**
37
+ * Supabase Auth error codes (`AuthError.code`) and the plain line for each.
38
+ * None says whether an account exists for an address, so a sign-in form
39
+ * can't be used to find out who has an account.
40
+ */
41
+ const AUTH_LINES = {
42
+ invalid_credentials: "That email address and password don't match. Check them and try again.",
43
+ otp_expired: "That code has expired or was already used. Ask for a new one.",
44
+ otp_disabled: "Signing in with a code isn't turned on.",
45
+ over_email_send_rate_limit: "Too many emails were sent to this address. Wait a few minutes, then try again.",
46
+ over_request_rate_limit: "Too many tries. Wait a few minutes, then try again.",
47
+ over_sms_send_rate_limit: "Too many messages were sent. Wait a few minutes, then try again.",
48
+ weak_password: "Choose a longer password that's harder to guess.",
49
+ same_password: "Choose a password different from your current one.",
50
+ reauthentication_needed: "For your safety, confirm it's you first. We'll send you a code.",
51
+ reauthentication_not_valid: "That code isn't right or has expired. Ask for a new one.",
52
+ mfa_verification_failed: "That code isn't right. Check your authenticator app and try again.",
53
+ mfa_challenge_expired: "That took too long. Start signing in again.",
54
+ session_expired: "You've been signed out. Sign in again.",
55
+ session_not_found: "You've been signed out. Sign in again.",
56
+ refresh_token_not_found: "You've been signed out. Sign in again.",
57
+ user_banned: "This login can't be used. Contact support.",
58
+ signup_disabled: "New accounts can't be made here. Ask the person who runs it for an invite.",
59
+ // These would say an account exists: the same neutral line as a new sign-up.
60
+ email_exists: "Check your email for a code to carry on.",
61
+ user_already_exists: "Check your email for a code to carry on.",
62
+ // An address Supabase won't take, said without saying why.
63
+ email_address_invalid: "Check the email address and try again.",
64
+ email_address_not_authorized: "Check the email address and try again.",
65
+ };
66
+ /**
67
+ * A plain line for a Supabase Auth error, by its code. An unknown code (or
68
+ * none) gets the app's plain server line, and the error is logged in full.
69
+ */
70
+ export function plainAuthError(error, opts) {
71
+ const code = error && typeof error === "object" ? error.code : undefined;
72
+ if (typeof code === "string" && Object.hasOwn(AUTH_LINES, code))
73
+ return AUTH_LINES[code];
74
+ (opts.log ?? console.error)(`[auth error shown as plain] ${typeof code === "string" ? `${code}: ` : ""}${messageOf(error) || String(error)}`);
75
+ return plainServerError(opts.appName);
76
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mccaskie-labs/workbench-supabase",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Supabase clients for Workbench apps (browser, server, the caller's own token, and a service client that logs its reason), plus check:migrations and check:prod-schema.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",