@ricsam/isolate-encoding 0.0.1 → 0.1.1

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 ADDED
@@ -0,0 +1,9 @@
1
+ # @ricsam/isolate-encoding
2
+
3
+ ## 0.1.1
4
+
5
+ ### Patch Changes
6
+
7
+ - initial release
8
+ - Updated dependencies
9
+ - @ricsam/isolate-core@0.1.1
package/package.json CHANGED
@@ -1,10 +1,29 @@
1
1
  {
2
2
  "name": "@ricsam/isolate-encoding",
3
- "version": "0.0.1",
4
- "description": "OIDC trusted publishing setup package for @ricsam/isolate-encoding",
5
- "keywords": [
6
- "oidc",
7
- "trusted-publishing",
8
- "setup"
9
- ]
3
+ "version": "0.1.1",
4
+ "type": "module",
5
+ "main": "./src/index.ts",
6
+ "types": "./src/index.ts",
7
+ "exports": {
8
+ ".": {
9
+ "import": "./src/index.ts",
10
+ "types": "./src/index.ts"
11
+ }
12
+ },
13
+ "scripts": {
14
+ "build": "tsc",
15
+ "test": "node --test --experimental-strip-types 'src/**/*.test.ts'",
16
+ "typecheck": "tsc --noEmit"
17
+ },
18
+ "dependencies": {
19
+ "@ricsam/isolate-core": "*",
20
+ "isolated-vm": "^6"
21
+ },
22
+ "devDependencies": {
23
+ "@types/node": "^24",
24
+ "typescript": "^5"
25
+ },
26
+ "peerDependencies": {
27
+ "isolated-vm": "^6"
28
+ }
10
29
  }
package/src/index.ts ADDED
@@ -0,0 +1,175 @@
1
+ import type ivm from "isolated-vm";
2
+
3
+ export interface EncodingHandle {
4
+ dispose(): void;
5
+ }
6
+
7
+ const encodingCode = `
8
+ (function() {
9
+ // Define DOMException if not available
10
+ if (typeof DOMException === 'undefined') {
11
+ globalThis.DOMException = class DOMException extends Error {
12
+ constructor(message, name) {
13
+ super(message);
14
+ this.name = name || 'DOMException';
15
+ }
16
+ };
17
+ }
18
+
19
+ const base64Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
20
+
21
+ // Build reverse lookup table
22
+ const base64Lookup = new Map();
23
+ for (let i = 0; i < base64Chars.length; i++) {
24
+ base64Lookup.set(base64Chars[i], i);
25
+ }
26
+
27
+ globalThis.btoa = function btoa(str) {
28
+ if (str === undefined) {
29
+ throw new TypeError("1 argument required, but only 0 present.");
30
+ }
31
+
32
+ str = String(str);
33
+
34
+ // Check for characters outside Latin-1 range
35
+ for (let i = 0; i < str.length; i++) {
36
+ if (str.charCodeAt(i) > 255) {
37
+ throw new DOMException(
38
+ "The string to be encoded contains characters outside of the Latin1 range.",
39
+ "InvalidCharacterError"
40
+ );
41
+ }
42
+ }
43
+
44
+ if (str.length === 0) {
45
+ return '';
46
+ }
47
+
48
+ let result = '';
49
+ let i = 0;
50
+
51
+ while (i < str.length) {
52
+ const a = str.charCodeAt(i++);
53
+ const bExists = i < str.length;
54
+ const b = bExists ? str.charCodeAt(i++) : 0;
55
+ const cExists = i < str.length;
56
+ const c = cExists ? str.charCodeAt(i++) : 0;
57
+
58
+ const triplet = (a << 16) | (b << 8) | c;
59
+
60
+ result += base64Chars[(triplet >> 18) & 0x3F];
61
+ result += base64Chars[(triplet >> 12) & 0x3F];
62
+ result += bExists ? base64Chars[(triplet >> 6) & 0x3F] : '=';
63
+ result += cExists ? base64Chars[triplet & 0x3F] : '=';
64
+ }
65
+
66
+ return result;
67
+ };
68
+
69
+ globalThis.atob = function atob(str) {
70
+ if (str === undefined) {
71
+ throw new TypeError("1 argument required, but only 0 present.");
72
+ }
73
+
74
+ str = String(str);
75
+
76
+ // Remove whitespace
77
+ str = str.replace(/[\\t\\n\\f\\r ]/g, '');
78
+
79
+ // Validate characters and length
80
+ if (str.length === 0) {
81
+ return '';
82
+ }
83
+
84
+ // Check for invalid characters (before padding normalization)
85
+ for (let i = 0; i < str.length; i++) {
86
+ const c = str[i];
87
+ if (c !== '=' && !base64Lookup.has(c)) {
88
+ throw new DOMException(
89
+ "The string to be decoded is not correctly encoded.",
90
+ "InvalidCharacterError"
91
+ );
92
+ }
93
+ }
94
+
95
+ // Validate padding position (must be at end)
96
+ const paddingIndex = str.indexOf('=');
97
+ if (paddingIndex !== -1) {
98
+ for (let i = paddingIndex; i < str.length; i++) {
99
+ if (str[i] !== '=') {
100
+ throw new DOMException(
101
+ "The string to be decoded is not correctly encoded.",
102
+ "InvalidCharacterError"
103
+ );
104
+ }
105
+ }
106
+ const paddingLength = str.length - paddingIndex;
107
+ if (paddingLength > 2) {
108
+ throw new DOMException(
109
+ "The string to be decoded is not correctly encoded.",
110
+ "InvalidCharacterError"
111
+ );
112
+ }
113
+ }
114
+
115
+ // Length without padding must be valid (can't have remainder of 1)
116
+ const strWithoutPadding = str.replace(/=/g, '');
117
+ if (strWithoutPadding.length % 4 === 1) {
118
+ throw new DOMException(
119
+ "The string to be decoded is not correctly encoded.",
120
+ "InvalidCharacterError"
121
+ );
122
+ }
123
+
124
+ // Pad to multiple of 4 if needed (for inputs without explicit padding)
125
+ while (str.length % 4 !== 0) {
126
+ str += '=';
127
+ }
128
+
129
+ let result = '';
130
+ let i = 0;
131
+
132
+ while (i < str.length) {
133
+ const a = base64Lookup.get(str[i++]) ?? 0;
134
+ const b = base64Lookup.get(str[i++]) ?? 0;
135
+ const c = base64Lookup.get(str[i++]) ?? 0;
136
+ const d = base64Lookup.get(str[i++]) ?? 0;
137
+
138
+ const triplet = (a << 18) | (b << 12) | (c << 6) | d;
139
+
140
+ result += String.fromCharCode((triplet >> 16) & 0xFF);
141
+ if (str[i - 2] !== '=') {
142
+ result += String.fromCharCode((triplet >> 8) & 0xFF);
143
+ }
144
+ if (str[i - 1] !== '=') {
145
+ result += String.fromCharCode(triplet & 0xFF);
146
+ }
147
+ }
148
+
149
+ return result;
150
+ };
151
+ })();
152
+ `;
153
+
154
+ /**
155
+ * Setup encoding APIs in an isolated-vm context
156
+ *
157
+ * Injects atob and btoa for Base64 encoding/decoding
158
+ *
159
+ * @example
160
+ * const handle = await setupEncoding(context);
161
+ * await context.eval(`
162
+ * const encoded = btoa("hello");
163
+ * const decoded = atob(encoded);
164
+ * `);
165
+ */
166
+ export async function setupEncoding(
167
+ context: ivm.Context
168
+ ): Promise<EncodingHandle> {
169
+ context.evalSync(encodingCode);
170
+ return {
171
+ dispose() {
172
+ // No resources to cleanup for pure JS injection
173
+ },
174
+ };
175
+ }
@@ -0,0 +1,134 @@
1
+ import { test, describe, beforeEach, afterEach } from "node:test";
2
+ import assert from "node:assert";
3
+ import ivm from "isolated-vm";
4
+ import { setupEncoding } from "./index.ts";
5
+
6
+ describe("@ricsam/isolate-encoding", () => {
7
+ let isolate: ivm.Isolate;
8
+ let context: ivm.Context;
9
+
10
+ beforeEach(async () => {
11
+ isolate = new ivm.Isolate();
12
+ context = await isolate.createContext();
13
+ });
14
+
15
+ afterEach(() => {
16
+ context.release();
17
+ isolate.dispose();
18
+ });
19
+
20
+ describe("btoa", () => {
21
+ test("encodes string to base64", async () => {
22
+ await setupEncoding(context);
23
+ const result = await context.eval(`btoa("hello")`);
24
+ assert.strictEqual(result, "aGVsbG8=");
25
+ });
26
+
27
+ test("handles empty string", async () => {
28
+ await setupEncoding(context);
29
+ const result = await context.eval(`btoa("")`);
30
+ assert.strictEqual(result, "");
31
+ });
32
+
33
+ test("handles Latin-1 characters", async () => {
34
+ await setupEncoding(context);
35
+ // Test with Latin-1 extended characters (char codes 128-255)
36
+ const result = await context.eval(`btoa("café")`);
37
+ // café uses é which is Latin-1 (char code 233)
38
+ assert.strictEqual(result, "Y2Fm6Q==");
39
+ });
40
+
41
+ test("throws on characters outside Latin-1 range", async () => {
42
+ await setupEncoding(context);
43
+ await assert.rejects(
44
+ async () => {
45
+ await context.eval(`btoa("hello 世界")`);
46
+ },
47
+ {
48
+ name: "InvalidCharacterError",
49
+ }
50
+ );
51
+ });
52
+
53
+ test("converts non-string arguments to string", async () => {
54
+ await setupEncoding(context);
55
+ const result = await context.eval(`btoa(123)`);
56
+ assert.strictEqual(result, "MTIz");
57
+ });
58
+ });
59
+
60
+ describe("atob", () => {
61
+ test("decodes base64 to string", async () => {
62
+ await setupEncoding(context);
63
+ const result = await context.eval(`atob("aGVsbG8=")`);
64
+ assert.strictEqual(result, "hello");
65
+ });
66
+
67
+ test("handles empty string", async () => {
68
+ await setupEncoding(context);
69
+ const result = await context.eval(`atob("")`);
70
+ assert.strictEqual(result, "");
71
+ });
72
+
73
+ test("throws on invalid base64", async () => {
74
+ await setupEncoding(context);
75
+ await assert.rejects(
76
+ async () => {
77
+ await context.eval(`atob("not valid base64!@#")`);
78
+ },
79
+ {
80
+ name: "InvalidCharacterError",
81
+ }
82
+ );
83
+ });
84
+
85
+ test("handles input without padding", async () => {
86
+ await setupEncoding(context);
87
+ // "aGVsbG8" is "hello" without the = padding
88
+ const result = await context.eval(`atob("aGVsbG8")`);
89
+ assert.strictEqual(result, "hello");
90
+ });
91
+
92
+ test("ignores whitespace in input", async () => {
93
+ await setupEncoding(context);
94
+ const result = await context.eval(`atob("aGVs bG8=")`);
95
+ assert.strictEqual(result, "hello");
96
+ });
97
+ });
98
+
99
+ describe("roundtrip", () => {
100
+ test("btoa and atob are inverse operations", async () => {
101
+ await setupEncoding(context);
102
+ const testStrings = [
103
+ "hello",
104
+ "Hello World!",
105
+ "test123",
106
+ "a",
107
+ "ab",
108
+ "abc",
109
+ "",
110
+ ];
111
+
112
+ for (const str of testStrings) {
113
+ const result = await context.eval(
114
+ `atob(btoa(${JSON.stringify(str)}))`
115
+ );
116
+ assert.strictEqual(result, str, `Roundtrip failed for: ${str}`);
117
+ }
118
+ });
119
+
120
+ test("handles binary data roundtrip", async () => {
121
+ await setupEncoding(context);
122
+ // Create a string with all Latin-1 bytes
123
+ const result = await context.eval(`
124
+ const bytes = [];
125
+ for (let i = 0; i < 256; i++) {
126
+ bytes.push(String.fromCharCode(i));
127
+ }
128
+ const str = bytes.join('');
129
+ atob(btoa(str)) === str;
130
+ `);
131
+ assert.strictEqual(result, true);
132
+ });
133
+ });
134
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "rootDir": "./src"
5
+ },
6
+ "include": ["src/**/*"],
7
+ "exclude": ["node_modules", "dist", "**/*.test.ts"]
8
+ }
package/README.md DELETED
@@ -1,45 +0,0 @@
1
- # @ricsam/isolate-encoding
2
-
3
- ## ⚠️ IMPORTANT NOTICE ⚠️
4
-
5
- **This package is created solely for the purpose of setting up OIDC (OpenID Connect) trusted publishing with npm.**
6
-
7
- This is **NOT** a functional package and contains **NO** code or functionality beyond the OIDC setup configuration.
8
-
9
- ## Purpose
10
-
11
- This package exists to:
12
- 1. Configure OIDC trusted publishing for the package name `@ricsam/isolate-encoding`
13
- 2. Enable secure, token-less publishing from CI/CD workflows
14
- 3. Establish provenance for packages published under this name
15
-
16
- ## What is OIDC Trusted Publishing?
17
-
18
- OIDC trusted publishing allows package maintainers to publish packages directly from their CI/CD workflows without needing to manage npm access tokens. Instead, it uses OpenID Connect to establish trust between the CI/CD provider (like GitHub Actions) and npm.
19
-
20
- ## Setup Instructions
21
-
22
- To properly configure OIDC trusted publishing for this package:
23
-
24
- 1. Go to [npmjs.com](https://www.npmjs.com/) and navigate to your package settings
25
- 2. Configure the trusted publisher (e.g., GitHub Actions)
26
- 3. Specify the repository and workflow that should be allowed to publish
27
- 4. Use the configured workflow to publish your actual package
28
-
29
- ## DO NOT USE THIS PACKAGE
30
-
31
- This package is a placeholder for OIDC configuration only. It:
32
- - Contains no executable code
33
- - Provides no functionality
34
- - Should not be installed as a dependency
35
- - Exists only for administrative purposes
36
-
37
- ## More Information
38
-
39
- For more details about npm's trusted publishing feature, see:
40
- - [npm Trusted Publishing Documentation](https://docs.npmjs.com/generating-provenance-statements)
41
- - [GitHub Actions OIDC Documentation](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect)
42
-
43
- ---
44
-
45
- **Maintained for OIDC setup purposes only**