@stackframe/stack-shared 2.8.1 → 2.8.3

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.
Files changed (39) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/interface/adminInterface.d.ts +7 -7
  3. package/dist/interface/adminInterface.js +4 -4
  4. package/dist/interface/clientInterface.d.ts +43 -1
  5. package/dist/interface/clientInterface.js +67 -3
  6. package/dist/interface/crud/current-user.d.ts +1 -1
  7. package/dist/interface/crud/{api-keys.d.ts → internal-api-keys.d.ts} +7 -7
  8. package/dist/interface/crud/{api-keys.js → internal-api-keys.js} +10 -10
  9. package/dist/interface/crud/project-api-keys.d.ts +188 -0
  10. package/dist/interface/crud/project-api-keys.js +76 -0
  11. package/dist/interface/crud/projects.d.ts +28 -0
  12. package/dist/interface/crud/projects.js +6 -0
  13. package/dist/interface/crud/team-member-profiles.d.ts +2 -2
  14. package/dist/interface/crud/users.d.ts +4 -4
  15. package/dist/interface/serverInterface.d.ts +2 -1
  16. package/dist/interface/serverInterface.js +4 -0
  17. package/dist/interface/webhooks.d.ts +34 -2
  18. package/dist/interface/webhooks.js +3 -0
  19. package/dist/known-errors.d.ts +24 -1
  20. package/dist/known-errors.js +30 -4
  21. package/dist/schema-fields.d.ts +1 -1
  22. package/dist/schema-fields.js +2 -1
  23. package/dist/utils/api-keys.d.ts +23 -0
  24. package/dist/utils/api-keys.js +75 -0
  25. package/dist/utils/bytes.d.ts +3 -0
  26. package/dist/utils/bytes.js +55 -6
  27. package/dist/utils/errors.js +1 -5
  28. package/dist/utils/hashes.d.ts +1 -1
  29. package/dist/utils/hashes.js +1 -3
  30. package/dist/utils/objects.d.ts +8 -0
  31. package/dist/utils/objects.js +11 -0
  32. package/dist/utils/results.js +5 -5
  33. package/dist/utils/strings.d.ts +1 -0
  34. package/dist/utils/strings.js +73 -17
  35. package/dist/utils/strings.nicify.test.js +214 -0
  36. package/dist/utils/types.d.ts +17 -0
  37. package/package.json +2 -1
  38. package/dist/utils/strings.test.js +0 -26
  39. /package/dist/utils/{strings.test.d.ts → strings.nicify.test.d.ts} +0 -0
@@ -68,11 +68,7 @@ StackAssertionError.prototype.name = "StackAssertionError";
68
68
  export function errorToNiceString(error) {
69
69
  if (!(error instanceof Error))
70
70
  return `${typeof error}<${nicify(error)}>`;
71
- let stack = error.stack ?? "";
72
- const toString = error.toString();
73
- if (!stack.startsWith(toString))
74
- stack = `${toString}\n${stack}`; // some browsers don't include the error message in the stack, some do
75
- return `${stack} ${nicify(Object.fromEntries(Object.entries(error)), { maxDepth: 8 })}`;
71
+ return nicify(error, { maxDepth: 8 });
76
72
  }
77
73
  const errorSinks = new Set();
78
74
  export function registerErrorSink(sink) {
@@ -1,4 +1,4 @@
1
- export declare function sha512(input: Uint8Array | string): Promise<string>;
1
+ export declare function sha512(input: Uint8Array | string): Promise<Uint8Array>;
2
2
  export declare function hashPassword(password: string): Promise<string>;
3
3
  export declare function comparePassword(password: string, hash: string): Promise<boolean>;
4
4
  export declare function isPasswordHashValid(hash: string): Promise<boolean>;
@@ -2,9 +2,7 @@ import bcrypt from 'bcrypt';
2
2
  import { StackAssertionError } from './errors';
3
3
  export async function sha512(input) {
4
4
  const bytes = typeof input === "string" ? new TextEncoder().encode(input) : input;
5
- return await crypto.subtle.digest("SHA-512", bytes).then(buf => {
6
- return Array.prototype.map.call(new Uint8Array(buf), x => (('00' + x.toString(16)).slice(-2))).join('');
7
- });
5
+ return new Uint8Array(await crypto.subtle.digest("SHA-512", bytes));
8
6
  }
9
7
  export async function hashPassword(password) {
10
8
  const passwordBytes = new TextEncoder().encode(password);
@@ -26,6 +26,14 @@ export type FilterUndefined<T> = {
26
26
  * TypeScript's `Partial<XYZ>` type allows `undefined` values.
27
27
  */
28
28
  export declare function filterUndefined<T extends {}>(obj: T): FilterUndefined<T>;
29
+ export type FilterUndefinedOrNull<T> = FilterUndefined<{
30
+ [k in keyof T]: null extends T[k] ? NonNullable<T[k]> | undefined : T[k];
31
+ }>;
32
+ /**
33
+ * Returns a new object with all undefined and null values removed. Useful when spreading optional parameters on an object, as
34
+ * TypeScript's `Partial<XYZ>` type allows `undefined` values.
35
+ */
36
+ export declare function filterUndefinedOrNull<T extends {}>(obj: T): FilterUndefinedOrNull<T>;
29
37
  export declare function pick<T extends {}, K extends keyof T>(obj: T, keys: K[]): Pick<T, K>;
30
38
  export declare function omit<T extends {}, K extends keyof T>(obj: T, keys: K[]): Omit<T, K>;
31
39
  export declare function split<T extends {}, K extends keyof T>(obj: T, keys: K[]): [Pick<T, K>, Omit<T, K>];
@@ -215,6 +215,17 @@ import.meta.vitest?.test("filterUndefined", ({ expect }) => {
215
215
  expect(filterUndefined({ a: null, b: undefined })).toEqual({ a: null });
216
216
  expect(filterUndefined({ a: 0, b: "", c: false, d: undefined })).toEqual({ a: 0, b: "", c: false });
217
217
  });
218
+ /**
219
+ * Returns a new object with all undefined and null values removed. Useful when spreading optional parameters on an object, as
220
+ * TypeScript's `Partial<XYZ>` type allows `undefined` values.
221
+ */
222
+ export function filterUndefinedOrNull(obj) {
223
+ return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== undefined && v !== null));
224
+ }
225
+ import.meta.vitest?.test("filterUndefinedOrNull", ({ expect }) => {
226
+ expect(filterUndefinedOrNull({})).toEqual({});
227
+ expect(filterUndefinedOrNull({ a: 1, b: 2 })).toEqual({ a: 1, b: 2 });
228
+ });
218
229
  export function pick(obj, keys) {
219
230
  return Object.fromEntries(Object.entries(obj).filter(([k]) => keys.includes(k)));
220
231
  }
@@ -1,5 +1,5 @@
1
1
  import { wait } from "./promises";
2
- import { deindent } from "./strings";
2
+ import { deindent, nicify } from "./strings";
3
3
  export const Result = {
4
4
  fromThrowing,
5
5
  fromThrowingAsync,
@@ -255,17 +255,17 @@ import.meta.vitest?.test("mapResult", ({ expect }) => {
255
255
  });
256
256
  class RetryError extends AggregateError {
257
257
  constructor(errors) {
258
- const strings = errors.map(e => String(e));
258
+ const strings = errors.map(e => nicify(e));
259
259
  const isAllSame = strings.length > 1 && strings.every(s => s === strings[0]);
260
260
  super(errors, deindent `
261
261
  Error after ${errors.length} attempts.
262
262
 
263
263
  ${isAllSame ? deindent `
264
264
  Attempts 1-${errors.length}:
265
- ${errors[0]}
266
- ` : errors.map((e, i) => deindent `
265
+ ${strings[0]}
266
+ ` : strings.map((s, i) => deindent `
267
267
  Attempt ${i + 1}:
268
- ${e}
268
+ ${s}
269
269
  `).join("\n\n")}
270
270
  `, { cause: errors[errors.length - 1] });
271
271
  this.errors = errors;
@@ -43,6 +43,7 @@ export declare function trimLines(s: string): string;
43
43
  export declare function templateIdentity(strings: TemplateStringsArray | readonly string[], ...values: string[]): string;
44
44
  export declare function deindent(code: string): string;
45
45
  export declare function deindent(strings: TemplateStringsArray | readonly string[], ...values: any[]): string;
46
+ export declare function deindentTemplate(strings: TemplateStringsArray | readonly string[], ...values: any[]): [string[], ...string[]];
46
47
  export declare function extractScopes(scope: string, removeDuplicates?: boolean): string[];
47
48
  export declare function mergeScopeStrings(...scopes: string[]): string;
48
49
  export declare function escapeTemplateLiteral(s: string): string;
@@ -207,8 +207,9 @@ Line2`).toBe("Line1\nLine2");
207
207
  export function deindent(strings, ...values) {
208
208
  if (typeof strings === "string")
209
209
  return deindent([strings]);
210
- if (strings.length === 0)
211
- return "";
210
+ return templateIdentity(...deindentTemplate(strings, ...values));
211
+ }
212
+ export function deindentTemplate(strings, ...values) {
212
213
  if (values.length !== strings.length - 1)
213
214
  throw new StackAssertionError("Invalid number of values; must be one less than strings", { strings, values });
214
215
  const trimmedStrings = [...strings];
@@ -231,7 +232,7 @@ export function deindent(strings, ...values) {
231
232
  const firstLineIndentation = getWhitespacePrefix(deindentedStrings[i].split("\n").at(-1));
232
233
  return `${value}`.replaceAll("\n", `\n${firstLineIndentation}`);
233
234
  });
234
- return templateIdentity(deindentedStrings, ...indentedValues);
235
+ return [deindentedStrings, ...indentedValues];
235
236
  }
236
237
  import.meta.vitest?.test("deindent", ({ expect }) => {
237
238
  // Test with string input
@@ -241,7 +242,6 @@ import.meta.vitest?.test("deindent", ({ expect }) => {
241
242
  expect(deindent("\n hello\n world\n")).toBe("hello\nworld");
242
243
  // Test with empty input
243
244
  expect(deindent("")).toBe("");
244
- expect(deindent([])).toBe("");
245
245
  // Test with template literal
246
246
  expect(deindent `
247
247
  hello
@@ -414,12 +414,13 @@ export function nicify(value, options = {}) {
414
414
  keyInParent: null,
415
415
  hideFields: [],
416
416
  };
417
- const nestedNicify = (newValue, newPath, keyInParent) => {
417
+ const nestedNicify = (newValue, newPath, keyInParent, options = {}) => {
418
418
  return nicify(newValue, {
419
419
  ...newOptions,
420
420
  path: newPath,
421
421
  currentIndent: currentIndent + lineIndent,
422
422
  keyInParent,
423
+ ...options,
423
424
  });
424
425
  };
425
426
  switch (typeof value) {
@@ -431,7 +432,7 @@ export function nicify(value, options = {}) {
431
432
  const isDeindentable = (v) => deindent(v) === v && v.includes("\n");
432
433
  const wrapInDeindent = (v) => deindent `
433
434
  deindent\`
434
- ${currentIndent + lineIndent}${escapeTemplateLiteral(value).replaceAll("\n", nl + lineIndent)}
435
+ ${currentIndent + lineIndent}${escapeTemplateLiteral(v).replaceAll("\n", nl + lineIndent)}
435
436
  ${currentIndent}\`
436
437
  `;
437
438
  if (isDeindentable(value)) {
@@ -470,7 +471,7 @@ export function nicify(value, options = {}) {
470
471
  resValues.push(...extraLines);
471
472
  if (resValues.length !== resValueLength)
472
473
  throw new StackAssertionError("nicify of object: resValues.length !== resValueLength", { value, resValues, resValueLength });
473
- const shouldIndent = resValues.length > 1 || resValues.some(x => x.includes("\n"));
474
+ const shouldIndent = resValues.length > 4 || resValues.some(x => (resValues.length > 1 && x.length > 4) || x.includes("\n"));
474
475
  if (shouldIndent) {
475
476
  return `[${nl}${resValues.map(x => `${lineIndent}${x},${nl}`).join("")}]`;
476
477
  }
@@ -479,13 +480,30 @@ export function nicify(value, options = {}) {
479
480
  }
480
481
  }
481
482
  if (value instanceof URL) {
482
- return `URL(${nicify(value.toString())})`;
483
+ return `URL(${nestedNicify(value.toString(), `${path}.toString()`, null)})`;
483
484
  }
484
485
  if (ArrayBuffer.isView(value)) {
485
486
  return `${value.constructor.name}([${value.toString()}])`;
486
487
  }
488
+ if (value instanceof Error) {
489
+ let stack = value.stack ?? "";
490
+ const toString = value.toString();
491
+ if (!stack.startsWith(toString))
492
+ stack = `${toString}\n${stack}`; // some browsers don't include the error message in the stack, some do
493
+ stack = stack.trimEnd();
494
+ stack = stack.replace(/\n\s+/g, `\n${lineIndent}${lineIndent}`);
495
+ stack = stack.replace("\n", `\n${lineIndent}Stack:\n`);
496
+ if (Object.keys(value).length > 0) {
497
+ stack += `\n${lineIndent}Extra properties: ${nestedNicify(Object.fromEntries(Object.entries(value)), path, null)}`;
498
+ }
499
+ if (value.cause) {
500
+ stack += `\n${lineIndent}Cause:\n${lineIndent}${lineIndent}${nestedNicify(value.cause, path, null, { currentIndent: currentIndent + lineIndent + lineIndent })}`;
501
+ }
502
+ stack = stack.replaceAll("\n", `\n${currentIndent}`);
503
+ return stack;
504
+ }
487
505
  const constructorName = [null, Object.prototype].includes(Object.getPrototypeOf(value)) ? null : (nicifiableClassNameOverrides.get(value.constructor) ?? value.constructor.name);
488
- const constructorString = constructorName ? `${nicifyPropertyString(constructorName)} ` : "";
506
+ const constructorString = constructorName ? `${constructorName} ` : "";
489
507
  const entries = getNicifiableEntries(value).filter(([k]) => !hideFields.includes(k));
490
508
  const extraLines = [
491
509
  ...getNicifiedObjectExtraLines(value),
@@ -498,7 +516,7 @@ export function nicify(value, options = {}) {
498
516
  return `${constructorString}{ ... }`;
499
517
  const resValues = entries.map(([k, v], keyIndex) => {
500
518
  const keyNicified = nestedNicify(k, `Object.keys(${path})[${keyIndex}]`, null);
501
- const keyInObjectLiteral = typeof k === "string" ? JSON.stringify(k) : `[${keyNicified}]`;
519
+ const keyInObjectLiteral = typeof k === "string" ? nicifyPropertyString(k) : `[${keyNicified}]`;
502
520
  if (typeof v === "function" && v.name === k) {
503
521
  return `${keyInObjectLiteral}(...): { ... }`;
504
522
  }
@@ -525,25 +543,63 @@ export function nicify(value, options = {}) {
525
543
  }
526
544
  }
527
545
  export function replaceAll(input, searchValue, replaceValue) {
546
+ if (searchValue === "")
547
+ throw new StackAssertionError("replaceAll: searchValue is empty");
528
548
  return input.split(searchValue).join(replaceValue);
529
549
  }
550
+ import.meta.vitest?.test("replaceAll", ({ expect }) => {
551
+ expect(replaceAll("hello world", "o", "x")).toBe("hellx wxrld");
552
+ expect(replaceAll("aaa", "a", "b")).toBe("bbb");
553
+ expect(replaceAll("", "a", "b")).toBe("");
554
+ expect(replaceAll("abc", "b", "")).toBe("ac");
555
+ expect(replaceAll("test.test.test", ".", "_")).toBe("test_test_test");
556
+ expect(replaceAll("a.b*c", ".", "x")).toBe("axb*c");
557
+ expect(replaceAll("a*b*c", "*", "x")).toBe("axbxc");
558
+ expect(replaceAll("hello hello", "hello", "hi")).toBe("hi hi");
559
+ });
530
560
  function nicifyPropertyString(str) {
531
- if (/^[_a-zA-Z][_a-zA-Z0-9]*$/.test(str))
532
- return str;
533
561
  return JSON.stringify(str);
534
562
  }
563
+ import.meta.vitest?.test("nicifyPropertyString", ({ expect }) => {
564
+ // Test valid identifiers
565
+ expect(nicifyPropertyString("validName")).toBe('"validName"');
566
+ expect(nicifyPropertyString("_validName")).toBe('"_validName"');
567
+ expect(nicifyPropertyString("valid123Name")).toBe('"valid123Name"');
568
+ // Test invalid identifiers
569
+ expect(nicifyPropertyString("123invalid")).toBe('"123invalid"');
570
+ expect(nicifyPropertyString("invalid-name")).toBe('"invalid-name"');
571
+ expect(nicifyPropertyString("invalid space")).toBe('"invalid space"');
572
+ expect(nicifyPropertyString("$invalid")).toBe('"$invalid"');
573
+ expect(nicifyPropertyString("")).toBe('""');
574
+ // Test with special characters
575
+ expect(nicifyPropertyString("property!")).toBe('"property!"');
576
+ expect(nicifyPropertyString("property.name")).toBe('"property.name"');
577
+ // Test with escaped characters
578
+ expect(nicifyPropertyString("\\")).toBe('"\\\\"');
579
+ expect(nicifyPropertyString('"')).toBe('"\\""');
580
+ });
535
581
  function getNicifiableKeys(value) {
536
582
  const overridden = ("getNicifiableKeys" in value ? value.getNicifiableKeys?.bind(value) : null)?.();
537
583
  if (overridden != null)
538
584
  return overridden;
539
585
  const keys = Object.keys(value).sort();
540
- if (value instanceof Error) {
541
- if (value.cause)
542
- keys.unshift("cause");
543
- keys.unshift("message", "stack");
544
- }
545
586
  return unique(keys);
546
587
  }
588
+ import.meta.vitest?.test("getNicifiableKeys", ({ expect }) => {
589
+ // Test regular object
590
+ expect(getNicifiableKeys({ b: 1, a: 2, c: 3 })).toEqual(["a", "b", "c"]);
591
+ // Test empty object
592
+ expect(getNicifiableKeys({})).toEqual([]);
593
+ // Test object with custom getNicifiableKeys
594
+ const customObject = {
595
+ a: 1,
596
+ b: 2,
597
+ getNicifiableKeys() {
598
+ return ["customKey1", "customKey2"];
599
+ }
600
+ };
601
+ expect(getNicifiableKeys(customObject)).toEqual(["customKey1", "customKey2"]);
602
+ });
547
603
  function getNicifiableEntries(value) {
548
604
  const recordLikes = [Headers];
549
605
  function isRecordLike(value) {
@@ -0,0 +1,214 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { deindent, nicify } from "./strings";
3
+ describe("nicify", () => {
4
+ describe("primitive values", () => {
5
+ test("numbers", () => {
6
+ expect(nicify(123)).toBe("123");
7
+ expect(nicify(123n)).toBe("123n");
8
+ });
9
+ test("strings", () => {
10
+ expect(nicify("hello")).toBe('"hello"');
11
+ });
12
+ test("booleans", () => {
13
+ expect(nicify(true)).toBe("true");
14
+ expect(nicify(false)).toBe("false");
15
+ });
16
+ test("null and undefined", () => {
17
+ expect(nicify(null)).toBe("null");
18
+ expect(nicify(undefined)).toBe("undefined");
19
+ });
20
+ test("symbols", () => {
21
+ expect(nicify(Symbol("test"))).toBe("Symbol(test)");
22
+ });
23
+ });
24
+ describe("arrays", () => {
25
+ test("empty array", () => {
26
+ expect(nicify([])).toBe("[]");
27
+ });
28
+ test("single-element array", () => {
29
+ expect(nicify([1])).toBe("[1]");
30
+ });
31
+ test("single-element array with long content", () => {
32
+ expect(nicify(["123123123123123"])).toBe('["123123123123123"]');
33
+ });
34
+ test("flat array", () => {
35
+ expect(nicify([1, 2, 3])).toBe("[1, 2, 3]");
36
+ });
37
+ test("longer array", () => {
38
+ expect(nicify([10000, 2, 3])).toBe(deindent `
39
+ [
40
+ 10000,
41
+ 2,
42
+ 3,
43
+ ]
44
+ `);
45
+ });
46
+ test("nested array", () => {
47
+ expect(nicify([1, [2, 3]])).toBe(deindent `
48
+ [
49
+ 1,
50
+ [2, 3],
51
+ ]
52
+ `);
53
+ });
54
+ });
55
+ describe("objects", () => {
56
+ test("empty object", () => {
57
+ expect(nicify({})).toBe("{}");
58
+ });
59
+ test("simple object", () => {
60
+ expect(nicify({ a: 1 })).toBe('{ "a": 1 }');
61
+ });
62
+ test("multiline object", () => {
63
+ expect(nicify({ a: 1, b: 2 })).toBe(deindent `
64
+ {
65
+ "a": 1,
66
+ "b": 2,
67
+ }
68
+ `);
69
+ });
70
+ });
71
+ describe("custom classes", () => {
72
+ test("class instance", () => {
73
+ class TestClass {
74
+ constructor(value) {
75
+ this.value = value;
76
+ }
77
+ }
78
+ expect(nicify(new TestClass(42))).toBe('TestClass { "value": 42 }');
79
+ });
80
+ });
81
+ describe("built-in objects", () => {
82
+ test("URL", () => {
83
+ expect(nicify(new URL("https://example.com"))).toBe('URL("https://example.com/")');
84
+ });
85
+ test("TypedArrays", () => {
86
+ expect(nicify(new Uint8Array([1, 2, 3]))).toBe("Uint8Array([1,2,3])");
87
+ expect(nicify(new Int32Array([1, 2, 3]))).toBe("Int32Array([1,2,3])");
88
+ });
89
+ test("Error objects", () => {
90
+ const error = new Error("test error");
91
+ const nicifiedError = nicify({ error });
92
+ expect(nicifiedError).toMatch(new RegExp(deindent `
93
+ ^\{
94
+ "error": Error: test error
95
+ Stack:
96
+ at (.|\\n)*
97
+ \}$
98
+ `));
99
+ });
100
+ test("Error objects with cause and an extra property", () => {
101
+ const error = new Error("test error", { cause: new Error("cause") });
102
+ error.extra = "something";
103
+ const nicifiedError = nicify(error, { lineIndent: "--" });
104
+ expect(nicifiedError).toMatch(new RegExp(deindent `
105
+ ^Error: test error
106
+ --Stack:
107
+ ----at (.|\\n)+
108
+ --Extra properties: \{ "extra": "something" \}
109
+ --Cause:
110
+ ----Error: cause
111
+ ------Stack:
112
+ --------at (.|\\n)+$
113
+ `));
114
+ });
115
+ test("Headers", () => {
116
+ const headers = new Headers();
117
+ headers.append("Content-Type", "application/json");
118
+ headers.append("Accept", "text/plain");
119
+ expect(nicify(headers)).toBe(deindent `
120
+ Headers {
121
+ "accept": "text/plain",
122
+ "content-type": "application/json",
123
+ }`);
124
+ });
125
+ });
126
+ describe("multiline strings", () => {
127
+ test("basic multiline", () => {
128
+ expect(nicify("line1\nline2")).toBe('deindent`\n line1\n line2\n`');
129
+ });
130
+ test("multiline with trailing newline", () => {
131
+ expect(nicify("line1\nline2\n")).toBe('deindent`\n line1\n line2\n` + "\\n"');
132
+ });
133
+ });
134
+ describe("circular references", () => {
135
+ test("object with self reference", () => {
136
+ const circular = { a: 1 };
137
+ circular.self = circular;
138
+ expect(nicify(circular)).toBe(deindent `
139
+ {
140
+ "a": 1,
141
+ "self": Ref<value>,
142
+ }`);
143
+ });
144
+ });
145
+ describe("configuration options", () => {
146
+ test("maxDepth", () => {
147
+ const deep = { a: { b: { c: { d: { e: 1 } } } } };
148
+ expect(nicify(deep, { maxDepth: 2 })).toBe('{ "a": { "b": { ... } } }');
149
+ });
150
+ test("lineIndent", () => {
151
+ expect(nicify({ a: 1, b: 2 }, { lineIndent: " " })).toBe(deindent `
152
+ {
153
+ "a": 1,
154
+ "b": 2,
155
+ }
156
+ `);
157
+ });
158
+ test("hideFields", () => {
159
+ expect(nicify({ a: 1, b: 2, secret: "hidden" }, { hideFields: ["secret"] })).toBe(deindent `
160
+ {
161
+ "a": 1,
162
+ "b": 2,
163
+ <some fields may have been hidden>,
164
+ }
165
+ `);
166
+ });
167
+ });
168
+ describe("custom overrides", () => {
169
+ test("override with custom type", () => {
170
+ expect(nicify({ type: "special" }, {
171
+ overrides: ((value) => {
172
+ if (typeof value === "object" && value && "type" in value && value.type === "special") {
173
+ return "SPECIAL";
174
+ }
175
+ return null;
176
+ })
177
+ })).toBe("SPECIAL");
178
+ });
179
+ });
180
+ describe("functions", () => {
181
+ test("named function", () => {
182
+ expect(nicify(function namedFunction() { })).toBe("function namedFunction(...) { ... }");
183
+ });
184
+ test("arrow function", () => {
185
+ expect(nicify(() => { })).toBe("(...) => { ... }");
186
+ });
187
+ });
188
+ describe("Nicifiable interface", () => {
189
+ test("object implementing Nicifiable", () => {
190
+ const nicifiable = {
191
+ value: 42,
192
+ getNicifiableKeys() {
193
+ return ["value"];
194
+ },
195
+ getNicifiedObjectExtraLines() {
196
+ return ["// custom comment"];
197
+ }
198
+ };
199
+ expect(nicify(nicifiable)).toBe(deindent `
200
+ {
201
+ "value": 42,
202
+ // custom comment,
203
+ }
204
+ `);
205
+ });
206
+ });
207
+ describe("unknown types", () => {
208
+ test("object without prototype", () => {
209
+ const unknownType = Object.create(null);
210
+ unknownType.value = "test";
211
+ expect(nicify(unknownType)).toBe('{ "value": "test" }');
212
+ });
213
+ });
214
+ });
@@ -2,3 +2,20 @@ export type IsAny<T> = 0 extends (1 & T) ? true : false;
2
2
  export type isNullish<T> = T extends null | undefined ? true : false;
3
3
  export type NullishCoalesce<T, U> = T extends null | undefined ? U : T;
4
4
  export type UnionToIntersection<U> = (U extends any ? (x: U) => void : never) extends ((x: infer I) => void) ? I : never;
5
+ /**
6
+ * A variation of TypeScript's conditionals with slightly different semantics. It is the perfect type for cases where:
7
+ *
8
+ * - If all possible values are contained in `Extends`, then it will be mapped to `Then`.
9
+ * - If all possible values are not contained in `Extends`, then it will be mapped to `Otherwise`.
10
+ * - If some possible values are contained in `Extends` and some are not, then it will be mapped to `Then | Otherwise`.
11
+ *
12
+ * This is different from TypeScript's built-in conditional types (`Value extends Extends ? Then : Otherwise`), which
13
+ * returns `Otherwise` for the third case (causing unsoundness in many real-world cases).
14
+ */
15
+ export type IfAndOnlyIf<Value, Extends, Then, Otherwise> = (Value extends Extends ? never : Otherwise) | (Value & Extends extends never ? never : Then);
16
+ /**
17
+ * Can be used to prettify a type in the IDE; for example, some complicated intersected types can be flattened into a single type.
18
+ */
19
+ export type PrettifyType<T> = {
20
+ [K in keyof T]: T[K];
21
+ } & {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stackframe/stack-shared",
3
- "version": "2.8.1",
3
+ "version": "2.8.3",
4
4
  "main": "./dist/index.js",
5
5
  "types": "./dist/index.d.ts",
6
6
  "type": "module",
@@ -45,6 +45,7 @@
45
45
  "@simplewebauthn/browser": "^11.0.0",
46
46
  "async-mutex": "^0.5.0",
47
47
  "bcrypt": "^5.1.1",
48
+ "crc": "^4.3.2",
48
49
  "elliptic": "^6.5.7",
49
50
  "ip-regex": "^5.0.0",
50
51
  "jose": "^5.2.2",
@@ -1,26 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { templateIdentity } from "./strings";
3
- describe("templateIdentity", () => {
4
- it("should be equivalent to a regular template string", () => {
5
- const adjective = "scientific";
6
- const noun = "railgun";
7
- expect(templateIdentity `a certain scientific railgun`).toBe("a certain scientific railgun");
8
- expect(templateIdentity `a certain ${adjective} railgun`).toBe(`a certain scientific railgun`);
9
- expect(templateIdentity `a certain ${adjective} ${noun}`).toBe(`a certain scientific railgun`);
10
- expect(templateIdentity `${adjective}${noun}`).toBe(`scientificrailgun`);
11
- });
12
- it("should work with empty strings", () => {
13
- expect(templateIdentity ``).toBe("");
14
- expect(templateIdentity `${""}`).toBe("");
15
- expect(templateIdentity `${""}${""}`).toBe("");
16
- });
17
- it("should work with normal arrays", () => {
18
- expect(templateIdentity(["a ", " scientific ", "gun"], "certain", "rail")).toBe("a certain scientific railgun");
19
- expect(templateIdentity(["a"])).toBe("a");
20
- });
21
- it("should throw an error with wrong number of value arguments", () => {
22
- expect(() => templateIdentity([])).toThrow();
23
- expect(() => templateIdentity(["a", "b"])).toThrow();
24
- expect(() => templateIdentity(["a", "b", "c"], "a", "b", "c")).toThrow();
25
- });
26
- });