@harborclient/sdk 0.4.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 (106) hide show
  1. package/README.md +41 -0
  2. package/dist/client.d.ts +2 -0
  3. package/dist/clipboard.d.ts +27 -0
  4. package/dist/clipboard.d.ts.map +1 -0
  5. package/dist/clipboard.js +17 -0
  6. package/dist/http/index.d.ts +3 -0
  7. package/dist/http/index.d.ts.map +1 -0
  8. package/dist/http/index.js +2 -0
  9. package/dist/http/resolveRequest.d.ts +66 -0
  10. package/dist/http/resolveRequest.d.ts.map +1 -0
  11. package/dist/http/resolveRequest.js +191 -0
  12. package/dist/http/resolveRequest.test.d.ts +2 -0
  13. package/dist/http/resolveRequest.test.d.ts.map +1 -0
  14. package/dist/http/resolveRequest.test.js +40 -0
  15. package/dist/http/substitute.d.ts +29 -0
  16. package/dist/http/substitute.d.ts.map +1 -0
  17. package/dist/http/substitute.js +43 -0
  18. package/dist/http/substitute.test.d.ts +2 -0
  19. package/dist/http/substitute.test.d.ts.map +1 -0
  20. package/dist/http/substitute.test.js +85 -0
  21. package/dist/index.d.ts +2 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +1 -0
  24. package/dist/main.d.ts +2 -0
  25. package/dist/main.d.ts.map +1 -0
  26. package/dist/main.js +1 -0
  27. package/dist/runtime/index.d.ts +20 -0
  28. package/dist/runtime/index.js +43 -0
  29. package/dist/runtime/jsx-dev-runtime.d.ts +12 -0
  30. package/dist/runtime/jsx-dev-runtime.js +15 -0
  31. package/dist/runtime/jsx-runtime.d.ts +35 -0
  32. package/dist/runtime/jsx-runtime.js +30 -0
  33. package/dist/runtime/react.d.ts +1 -0
  34. package/dist/runtime/react.js +46 -0
  35. package/dist/runtime/reactHost.js +26 -0
  36. package/dist/runtime/store.d.ts +19 -0
  37. package/dist/runtime/store.d.ts.map +1 -0
  38. package/dist/runtime/store.js +38 -0
  39. package/dist/runtime/store.ts +45 -0
  40. package/dist/runtime-utils.d.ts +36 -0
  41. package/dist/runtime-utils.d.ts.map +1 -0
  42. package/dist/runtime-utils.js +101 -0
  43. package/dist/runtime-utils.test.d.ts +2 -0
  44. package/dist/runtime-utils.test.d.ts.map +1 -0
  45. package/dist/runtime-utils.test.js +104 -0
  46. package/dist/signing/canonical.d.ts +27 -0
  47. package/dist/signing/canonical.d.ts.map +1 -0
  48. package/dist/signing/canonical.js +92 -0
  49. package/dist/signing/cli-sign.d.ts +3 -0
  50. package/dist/signing/cli-sign.d.ts.map +1 -0
  51. package/dist/signing/cli-sign.js +4 -0
  52. package/dist/signing/cli-verify.d.ts +3 -0
  53. package/dist/signing/cli-verify.d.ts.map +1 -0
  54. package/dist/signing/cli-verify.js +4 -0
  55. package/dist/signing/cli.d.ts +13 -0
  56. package/dist/signing/cli.d.ts.map +1 -0
  57. package/dist/signing/cli.js +148 -0
  58. package/dist/signing/index.d.ts +10 -0
  59. package/dist/signing/index.d.ts.map +1 -0
  60. package/dist/signing/index.js +7 -0
  61. package/dist/signing/inventory.d.ts +21 -0
  62. package/dist/signing/inventory.d.ts.map +1 -0
  63. package/dist/signing/inventory.js +80 -0
  64. package/dist/signing/manifest.d.ts +16 -0
  65. package/dist/signing/manifest.d.ts.map +1 -0
  66. package/dist/signing/manifest.js +33 -0
  67. package/dist/signing/sign.d.ts +10 -0
  68. package/dist/signing/sign.d.ts.map +1 -0
  69. package/dist/signing/sign.js +48 -0
  70. package/dist/signing/signing.test.d.ts +2 -0
  71. package/dist/signing/signing.test.d.ts.map +1 -0
  72. package/dist/signing/signing.test.js +100 -0
  73. package/dist/signing/testFixtures.d.ts +21 -0
  74. package/dist/signing/testFixtures.d.ts.map +1 -0
  75. package/dist/signing/testFixtures.js +41 -0
  76. package/dist/signing/types.d.ts +87 -0
  77. package/dist/signing/types.d.ts.map +1 -0
  78. package/dist/signing/types.js +12 -0
  79. package/dist/signing/verify.d.ts +17 -0
  80. package/dist/signing/verify.d.ts.map +1 -0
  81. package/dist/signing/verify.js +129 -0
  82. package/dist/storage/cappedList.d.ts +55 -0
  83. package/dist/storage/cappedList.d.ts.map +1 -0
  84. package/dist/storage/cappedList.js +102 -0
  85. package/dist/storage/cappedList.test.d.ts +2 -0
  86. package/dist/storage/cappedList.test.d.ts.map +1 -0
  87. package/dist/storage/cappedList.test.js +11 -0
  88. package/dist/storage/index.d.ts +2 -0
  89. package/dist/storage/index.d.ts.map +1 -0
  90. package/dist/storage/index.js +1 -0
  91. package/dist/types.d.ts +1282 -0
  92. package/dist/types.d.ts.map +1 -0
  93. package/dist/types.js +1 -0
  94. package/dist/ui/format.d.ts +23 -0
  95. package/dist/ui/format.d.ts.map +1 -0
  96. package/dist/ui/format.js +45 -0
  97. package/dist/ui/index.d.ts +3 -0
  98. package/dist/ui/index.d.ts.map +1 -0
  99. package/dist/ui/index.js +2 -0
  100. package/dist/ui/tokens.d.ts +34 -0
  101. package/dist/ui/tokens.d.ts.map +1 -0
  102. package/dist/ui/tokens.js +56 -0
  103. package/dist/utilities.test.d.ts +2 -0
  104. package/dist/utilities.test.d.ts.map +1 -0
  105. package/dist/utilities.test.js +16 -0
  106. package/package.json +130 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"testFixtures.d.ts","sourceRoot":"","sources":["../../src/signing/testFixtures.ts"],"names":[],"mappings":"AAKA;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED;;GAEG;AACH,wBAAgB,qBAAqB,IAAI,eAAe,CASvD;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,SAA4B,GAAG;IACzE,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB,CA0BA"}
@@ -0,0 +1,41 @@
1
+ import { generateKeyPairSync } from 'node:crypto';
2
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ /**
6
+ * Generates an Ed25519 PEM key pair for tests.
7
+ */
8
+ export function createTestSigningKeys() {
9
+ const pair = generateKeyPairSync('ed25519', {
10
+ publicKeyEncoding: { type: 'spki', format: 'pem' },
11
+ privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
12
+ });
13
+ return {
14
+ privateKeyPem: pair.privateKey,
15
+ publicKeyPem: pair.publicKey
16
+ };
17
+ }
18
+ /**
19
+ * Creates a minimal plugin directory suitable for signing tests.
20
+ *
21
+ * @param pluginId - Plugin manifest id.
22
+ */
23
+ export function createTestPluginDir(pluginId = 'com.example.test-plugin') {
24
+ const pluginDir = mkdtempSync(join(tmpdir(), 'hc-plugin-sign-'));
25
+ mkdirSync(join(pluginDir, 'dist'), { recursive: true });
26
+ writeFileSync(join(pluginDir, 'manifest.json'), JSON.stringify({
27
+ id: pluginId,
28
+ name: 'Test Plugin',
29
+ version: '1.0.0',
30
+ engines: { harborclient: '>=1.0.0' },
31
+ renderer: 'dist/renderer.js',
32
+ permissions: ['ui']
33
+ }, null, 2));
34
+ writeFileSync(join(pluginDir, 'dist', 'renderer.js'), 'export function activate() {}');
35
+ return {
36
+ pluginDir,
37
+ cleanup: () => {
38
+ rmSync(pluginDir, { recursive: true, force: true });
39
+ }
40
+ };
41
+ }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Filename written at the plugin root when a package is signed.
3
+ */
4
+ export declare const PLUGIN_SIGNATURE_FILENAME = "signature.json";
5
+ /**
6
+ * Supported plugin signature schema version.
7
+ */
8
+ export declare const PLUGIN_SIGNATURE_SCHEMA_VERSION = 1;
9
+ /**
10
+ * Signature algorithm stored in signature.json.
11
+ */
12
+ export declare const PLUGIN_SIGNATURE_ALGORITHM: "Ed25519";
13
+ /**
14
+ * One hashed file entry in a plugin signature inventory.
15
+ */
16
+ export interface PluginFileHash {
17
+ path: string;
18
+ sha256: string;
19
+ }
20
+ /**
21
+ * Parsed plugin signature.json on disk (excluding runtime-only fields).
22
+ */
23
+ export interface PluginSignatureFile {
24
+ schemaVersion: typeof PLUGIN_SIGNATURE_SCHEMA_VERSION;
25
+ pluginId: string;
26
+ pluginVersion: string;
27
+ algorithm: typeof PLUGIN_SIGNATURE_ALGORITHM;
28
+ keyId?: string;
29
+ files: PluginFileHash[];
30
+ signature: string;
31
+ }
32
+ /**
33
+ * Payload signed by Ed25519 before the signature field is attached.
34
+ */
35
+ export interface PluginSignaturePayload {
36
+ schemaVersion: typeof PLUGIN_SIGNATURE_SCHEMA_VERSION;
37
+ pluginId: string;
38
+ pluginVersion: string;
39
+ algorithm: typeof PLUGIN_SIGNATURE_ALGORITHM;
40
+ keyId?: string;
41
+ files: PluginFileHash[];
42
+ }
43
+ /**
44
+ * Result of verifying a plugin signature.
45
+ */
46
+ export type PluginVerifyStatus = 'valid' | 'unsigned' | 'invalid';
47
+ /**
48
+ * Options for {@link signPlugin}.
49
+ */
50
+ export interface SignPluginOptions {
51
+ /** Absolute or relative path to the plugin root directory. */
52
+ pluginDir: string;
53
+ /** PEM-encoded Ed25519 private key. */
54
+ privateKeyPem: string;
55
+ /** Optional label stored in signature.json to identify the signing key. */
56
+ keyId?: string;
57
+ /** Override path for signature.json (default: `<pluginDir>/signature.json`). */
58
+ signaturePath?: string;
59
+ }
60
+ /**
61
+ * Result returned after writing a plugin signature file.
62
+ */
63
+ export interface SignPluginResult {
64
+ signaturePath: string;
65
+ signature: PluginSignatureFile;
66
+ }
67
+ /**
68
+ * Options for {@link verifyPlugin}.
69
+ */
70
+ export interface VerifyPluginOptions {
71
+ /** Absolute or relative path to the plugin root directory. */
72
+ pluginDir: string;
73
+ /** PEM-encoded Ed25519 public keys trusted for verification. */
74
+ trustedPublicKeysPem: string[];
75
+ /** Override path for signature.json (default: `<pluginDir>/signature.json`). */
76
+ signaturePath?: string;
77
+ }
78
+ /**
79
+ * Result returned after verifying a plugin signature.
80
+ */
81
+ export interface VerifyPluginResult {
82
+ status: PluginVerifyStatus;
83
+ signature?: PluginSignatureFile;
84
+ keyId?: string;
85
+ error?: string;
86
+ }
87
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/signing/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,eAAO,MAAM,yBAAyB,mBAAmB,CAAC;AAE1D;;GAEG;AACH,eAAO,MAAM,+BAA+B,IAAI,CAAC;AAEjD;;GAEG;AACH,eAAO,MAAM,0BAA0B,EAAG,SAAkB,CAAC;AAE7D;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,aAAa,EAAE,OAAO,+BAA+B,CAAC;IACtD,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,OAAO,0BAA0B,CAAC;IAC7C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC,aAAa,EAAE,OAAO,+BAA+B,CAAC;IACtD,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,OAAO,0BAA0B,CAAC;IAC7C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,cAAc,EAAE,CAAC;CACzB;AAED;;GAEG;AACH,MAAM,MAAM,kBAAkB,GAAG,OAAO,GAAG,UAAU,GAAG,SAAS,CAAC;AAElE;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,8DAA8D;IAC9D,SAAS,EAAE,MAAM,CAAC;IAClB,uCAAuC;IACvC,aAAa,EAAE,MAAM,CAAC;IACtB,2EAA2E;IAC3E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,gFAAgF;IAChF,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,mBAAmB,CAAC;CAChC;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,8DAA8D;IAC9D,SAAS,EAAE,MAAM,CAAC;IAClB,gEAAgE;IAChE,oBAAoB,EAAE,MAAM,EAAE,CAAC;IAC/B,gFAAgF;IAChF,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,kBAAkB,CAAC;IAC3B,SAAS,CAAC,EAAE,mBAAmB,CAAC;IAChC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Filename written at the plugin root when a package is signed.
3
+ */
4
+ export const PLUGIN_SIGNATURE_FILENAME = 'signature.json';
5
+ /**
6
+ * Supported plugin signature schema version.
7
+ */
8
+ export const PLUGIN_SIGNATURE_SCHEMA_VERSION = 1;
9
+ /**
10
+ * Signature algorithm stored in signature.json.
11
+ */
12
+ export const PLUGIN_SIGNATURE_ALGORITHM = 'Ed25519';
@@ -0,0 +1,17 @@
1
+ import type { PluginSignatureFile, VerifyPluginOptions, VerifyPluginResult } from './types.js';
2
+ /**
3
+ * Reads signature.json from a plugin directory when present.
4
+ *
5
+ * @param pluginDir - Plugin root directory.
6
+ * @returns Parsed signature file or null when signature.json is absent.
7
+ * @throws When signature.json exists but is invalid JSON or shape.
8
+ */
9
+ export declare function readPluginSignature(pluginDir: string): PluginSignatureFile | null;
10
+ /**
11
+ * Verifies a plugin signature against trusted public keys and on-disk file hashes.
12
+ *
13
+ * @param options - Plugin directory and trusted public key options.
14
+ * @returns Verification status with optional error detail.
15
+ */
16
+ export declare function verifyPlugin(options: VerifyPluginOptions): Promise<VerifyPluginResult>;
17
+ //# sourceMappingURL=verify.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verify.d.ts","sourceRoot":"","sources":["../../src/signing/verify.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAE/F;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,GAAG,mBAAmB,GAAG,IAAI,CAcjF;AAED;;;;;GAKG;AACH,wBAAsB,YAAY,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CA0F5F"}
@@ -0,0 +1,129 @@
1
+ import { createPublicKey, verify } from 'node:crypto';
2
+ import { existsSync, readFileSync } from 'node:fs';
3
+ import { resolve } from 'node:path';
4
+ import { buildSignaturePayload, canonicalizeSignaturePayload, parsePluginSignatureFile } from './canonical.js';
5
+ import { assertPluginDirectory, collectPluginFiles } from './inventory.js';
6
+ import { readPluginManifestIdentity } from './manifest.js';
7
+ import { PLUGIN_SIGNATURE_FILENAME } from './types.js';
8
+ /**
9
+ * Reads signature.json from a plugin directory when present.
10
+ *
11
+ * @param pluginDir - Plugin root directory.
12
+ * @returns Parsed signature file or null when signature.json is absent.
13
+ * @throws When signature.json exists but is invalid JSON or shape.
14
+ */
15
+ export function readPluginSignature(pluginDir) {
16
+ const signaturePath = resolve(pluginDir, PLUGIN_SIGNATURE_FILENAME);
17
+ if (!existsSync(signaturePath)) {
18
+ return null;
19
+ }
20
+ let raw;
21
+ try {
22
+ raw = JSON.parse(readFileSync(signaturePath, 'utf8'));
23
+ }
24
+ catch (error) {
25
+ throw new Error(`Plugin signature is not valid JSON: ${signaturePath}`, { cause: error });
26
+ }
27
+ return parsePluginSignatureFile(raw);
28
+ }
29
+ /**
30
+ * Verifies a plugin signature against trusted public keys and on-disk file hashes.
31
+ *
32
+ * @param options - Plugin directory and trusted public key options.
33
+ * @returns Verification status with optional error detail.
34
+ */
35
+ export async function verifyPlugin(options) {
36
+ const pluginDir = resolve(options.pluginDir);
37
+ assertPluginDirectory(pluginDir);
38
+ const signaturePath = resolve(options.signaturePath ?? resolve(pluginDir, PLUGIN_SIGNATURE_FILENAME));
39
+ if (!existsSync(signaturePath)) {
40
+ return { status: 'unsigned' };
41
+ }
42
+ let signature;
43
+ try {
44
+ const raw = JSON.parse(readFileSync(signaturePath, 'utf8'));
45
+ signature = parsePluginSignatureFile(raw);
46
+ }
47
+ catch (error) {
48
+ const message = error instanceof Error ? error.message : String(error);
49
+ return { status: 'invalid', error: message };
50
+ }
51
+ let manifestIdentity;
52
+ try {
53
+ manifestIdentity = readPluginManifestIdentity(pluginDir);
54
+ }
55
+ catch (error) {
56
+ const message = error instanceof Error ? error.message : String(error);
57
+ return { status: 'invalid', signature, error: message };
58
+ }
59
+ if (signature.pluginId !== manifestIdentity.id) {
60
+ return {
61
+ status: 'invalid',
62
+ signature,
63
+ error: `Signature pluginId "${signature.pluginId}" does not match manifest id "${manifestIdentity.id}".`
64
+ };
65
+ }
66
+ if (signature.pluginVersion !== manifestIdentity.version) {
67
+ return {
68
+ status: 'invalid',
69
+ signature,
70
+ error: `Signature pluginVersion "${signature.pluginVersion}" does not match manifest version "${manifestIdentity.version}".`
71
+ };
72
+ }
73
+ const currentFiles = collectPluginFiles(pluginDir);
74
+ if (!fileInventoriesMatch(signature.files, currentFiles)) {
75
+ return {
76
+ status: 'invalid',
77
+ signature,
78
+ error: 'Plugin files do not match the signed inventory.'
79
+ };
80
+ }
81
+ const payload = buildSignaturePayload(signature.pluginId, signature.pluginVersion, signature.files, signature.keyId);
82
+ const payloadBytes = canonicalizeSignaturePayload(payload);
83
+ const signatureBytes = new Uint8Array(Buffer.from(signature.signature, 'base64'));
84
+ if (options.trustedPublicKeysPem.length === 0) {
85
+ return {
86
+ status: 'invalid',
87
+ signature,
88
+ error: 'At least one trusted public key is required for verification.'
89
+ };
90
+ }
91
+ for (const publicKeyPem of options.trustedPublicKeysPem) {
92
+ try {
93
+ const publicKey = createPublicKey(publicKeyPem);
94
+ if (verify(null, new Uint8Array(payloadBytes), publicKey, signatureBytes)) {
95
+ return {
96
+ status: 'valid',
97
+ signature,
98
+ keyId: signature.keyId
99
+ };
100
+ }
101
+ }
102
+ catch {
103
+ continue;
104
+ }
105
+ }
106
+ return {
107
+ status: 'invalid',
108
+ signature,
109
+ error: 'Plugin signature failed verification against all trusted public keys.'
110
+ };
111
+ }
112
+ /**
113
+ * Returns true when two plugin inventories contain identical path/hash pairs.
114
+ *
115
+ * @param signedFiles - File inventory stored in signature.json.
116
+ * @param currentFiles - File inventory computed from disk.
117
+ */
118
+ function fileInventoriesMatch(signedFiles, currentFiles) {
119
+ if (signedFiles.length !== currentFiles.length) {
120
+ return false;
121
+ }
122
+ for (let index = 0; index < signedFiles.length; index += 1) {
123
+ if (signedFiles[index].path !== currentFiles[index].path ||
124
+ signedFiles[index].sha256 !== currentFiles[index].sha256) {
125
+ return false;
126
+ }
127
+ }
128
+ return true;
129
+ }
@@ -0,0 +1,55 @@
1
+ import type { PluginStorage } from '../types.js';
2
+ /**
3
+ * Options for {@link mergeById}.
4
+ */
5
+ export interface MergeByIdOptions<T> {
6
+ /**
7
+ * Maximum number of entries to retain after merging.
8
+ */
9
+ cap: number;
10
+ /**
11
+ * Returns the stable id used for deduplication.
12
+ */
13
+ idOf: (entry: T) => string;
14
+ }
15
+ /**
16
+ * Merges pending entries ahead of existing ones, deduping by id and capping length.
17
+ *
18
+ * @param pending - New entries to prepend (newest first).
19
+ * @param existing - Previously stored entries (newest first).
20
+ * @param options - Cap and id selector.
21
+ */
22
+ export declare function mergeById<T>(pending: T[], existing: T[], options: MergeByIdOptions<T>): T[];
23
+ /**
24
+ * Options for {@link createCappedList}.
25
+ */
26
+ export interface CreateCappedListOptions<T> {
27
+ /**
28
+ * Plugin storage backing the list.
29
+ */
30
+ storage: PluginStorage;
31
+ /**
32
+ * Storage key within the plugin namespace.
33
+ */
34
+ key: string;
35
+ /**
36
+ * Maximum number of entries to persist.
37
+ */
38
+ cap: number;
39
+ /**
40
+ * Returns the stable id used for deduplication.
41
+ */
42
+ idOf: (entry: T) => string;
43
+ }
44
+ /**
45
+ * Persistent capped list helper with atomic read-modify-write semantics.
46
+ *
47
+ * @param options - Storage, key, cap, and id selector.
48
+ */
49
+ export declare function createCappedList<T>(options: CreateCappedListOptions<T>): {
50
+ load: () => Promise<T[]>;
51
+ merge: (pending: T[]) => Promise<T[] | null>;
52
+ save: (entries: T[]) => Promise<void>;
53
+ clear: () => Promise<void>;
54
+ };
55
+ //# sourceMappingURL=cappedList.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cappedList.d.ts","sourceRoot":"","sources":["../../src/storage/cappedList.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD;;GAEG;AACH,MAAM,WAAW,gBAAgB,CAAC,CAAC;IACjC;;OAEG;IACH,GAAG,EAAE,MAAM,CAAC;IAEZ;;OAEG;IACH,IAAI,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,MAAM,CAAC;CAC5B;AAED;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,EAAE,QAAQ,EAAE,CAAC,EAAE,EAAE,OAAO,EAAE,gBAAgB,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAkB3F;AAED;;GAEG;AACH,MAAM,WAAW,uBAAuB,CAAC,CAAC;IACxC;;OAEG;IACH,OAAO,EAAE,aAAa,CAAC;IAEvB;;OAEG;IACH,GAAG,EAAE,MAAM,CAAC;IAEZ;;OAEG;IACH,GAAG,EAAE,MAAM,CAAC;IAEZ;;OAEG;IACH,IAAI,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,MAAM,CAAC;CAC5B;AAkCD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,OAAO,EAAE,uBAAuB,CAAC,CAAC,CAAC,GAAG;IACxE,IAAI,EAAE,MAAM,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC;IACzB,KAAK,EAAE,CAAC,OAAO,EAAE,CAAC,EAAE,KAAK,OAAO,CAAC,CAAC,EAAE,GAAG,IAAI,CAAC,CAAC;IAC7C,IAAI,EAAE,CAAC,OAAO,EAAE,CAAC,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACtC,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5B,CA0CA"}
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Merges pending entries ahead of existing ones, deduping by id and capping length.
3
+ *
4
+ * @param pending - New entries to prepend (newest first).
5
+ * @param existing - Previously stored entries (newest first).
6
+ * @param options - Cap and id selector.
7
+ */
8
+ export function mergeById(pending, existing, options) {
9
+ if (pending.length === 0) {
10
+ return existing.slice(0, options.cap);
11
+ }
12
+ const seen = new Set();
13
+ const merged = [];
14
+ for (const entry of [...pending, ...existing]) {
15
+ const id = options.idOf(entry);
16
+ if (seen.has(id)) {
17
+ continue;
18
+ }
19
+ seen.add(id);
20
+ merged.push(entry);
21
+ if (merged.length >= options.cap) {
22
+ break;
23
+ }
24
+ }
25
+ return merged;
26
+ }
27
+ /**
28
+ * Serialized read-modify-write queue keyed by storage key.
29
+ */
30
+ const writeQueues = new Map();
31
+ /**
32
+ * Enqueues one storage mutation so concurrent callers cannot interleave get/set.
33
+ *
34
+ * @param key - Storage key used as the queue id.
35
+ * @param operation - Read-modify-write work to serialize.
36
+ */
37
+ async function enqueueStorageWrite(key, operation) {
38
+ const previous = writeQueues.get(key) ?? Promise.resolve();
39
+ let resolveDone;
40
+ const done = new Promise((resolve) => {
41
+ resolveDone = resolve;
42
+ });
43
+ writeQueues.set(key, previous.then(() => done));
44
+ await previous;
45
+ try {
46
+ return await operation();
47
+ }
48
+ finally {
49
+ resolveDone();
50
+ if (writeQueues.get(key) === done) {
51
+ writeQueues.delete(key);
52
+ }
53
+ }
54
+ }
55
+ /**
56
+ * Persistent capped list helper with atomic read-modify-write semantics.
57
+ *
58
+ * @param options - Storage, key, cap, and id selector.
59
+ */
60
+ export function createCappedList(options) {
61
+ const queueKey = options.key;
62
+ return {
63
+ load: async () => {
64
+ const saved = await options.storage.get(options.key);
65
+ return Array.isArray(saved) ? saved.slice(0, options.cap) : [];
66
+ },
67
+ merge: async (pending) => {
68
+ if (pending.length === 0) {
69
+ return null;
70
+ }
71
+ return enqueueStorageWrite(queueKey, async () => {
72
+ const existing = await options.storage.get(options.key);
73
+ const current = Array.isArray(existing) ? existing : [];
74
+ const merged = mergeById(pending, current, options);
75
+ if (merged.length === current.length) {
76
+ let unchanged = true;
77
+ for (let index = 0; index < merged.length; index += 1) {
78
+ if (options.idOf(merged[index]) !== options.idOf(current[index])) {
79
+ unchanged = false;
80
+ break;
81
+ }
82
+ }
83
+ if (unchanged) {
84
+ return null;
85
+ }
86
+ }
87
+ await options.storage.set(options.key, merged);
88
+ return merged;
89
+ });
90
+ },
91
+ save: async (entries) => {
92
+ await enqueueStorageWrite(queueKey, async () => {
93
+ await options.storage.set(options.key, entries.slice(0, options.cap));
94
+ });
95
+ },
96
+ clear: async () => {
97
+ await enqueueStorageWrite(queueKey, async () => {
98
+ await options.storage.set(options.key, []);
99
+ });
100
+ }
101
+ };
102
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=cappedList.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cappedList.test.d.ts","sourceRoot":"","sources":["../../src/storage/cappedList.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,11 @@
1
+ import { describe, expect, it } from '@jest/globals';
2
+ import { mergeById } from './cappedList.js';
3
+ describe('mergeById', () => {
4
+ it('dedupes and caps newest-first', () => {
5
+ const merged = mergeById([{ id: 'b' }, { id: 'a' }], [{ id: 'a' }, { id: 'c' }], {
6
+ cap: 3,
7
+ idOf: (entry) => entry.id
8
+ });
9
+ expect(merged.map((entry) => entry.id)).toEqual(['b', 'a', 'c']);
10
+ });
11
+ });
@@ -0,0 +1,2 @@
1
+ export { createCappedList, mergeById, type CreateCappedListOptions, type MergeByIdOptions } from './cappedList.js';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/storage/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,gBAAgB,EAChB,SAAS,EACT,KAAK,uBAAuB,EAC5B,KAAK,gBAAgB,EACtB,MAAM,iBAAiB,CAAC"}
@@ -0,0 +1 @@
1
+ export { createCappedList, mergeById } from './cappedList.js';