Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 72 additions & 2 deletions .github/workflows/prerelease.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
name: Vana SDK Pre-release

on:
workflow_dispatch:
inputs:
pr_number:
description: "PR number to publish, e.g. 147"
required: true
type: number
push:
branches:
- feature/data-portability-sdk-v1
Expand All @@ -12,7 +18,73 @@ on:
- remove-subgraph

jobs:
publish-pr-prerelease:
if: github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
steps:
- name: Resolve PR head
id: pr
uses: actions/github-script@v7
env:
PR_NUMBER: ${{ inputs.pr_number }}
with:
script: |
const prNumber = Number(process.env.PR_NUMBER);
const { owner, repo } = context.repo;
const { data: pull } = await github.rest.pulls.get({
owner,
repo,
pull_number: prNumber,
});

if (pull.head.repo.full_name !== `${owner}/${repo}`) {
core.setFailed(
`Refusing to publish prerelease for fork PR ${prNumber}: ${pull.head.repo.full_name}`,
);
return;
}

core.setOutput('head_sha', pull.head.sha);
core.setOutput('dist_tag', `pr-${prNumber}`);
core.setOutput('version_suffix', `pr.${prNumber}.${pull.head.sha.slice(0, 7)}`);

- name: Checkout PR head
uses: actions/checkout@v4
with:
ref: ${{ steps.pr.outputs.head_sha }}

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "22"
registry-url: "https://registry.npmjs.org"
cache: "npm"

- name: Install dependencies
run: npm ci

- name: Set PR pre-release version
run: |
cd packages/vana-sdk
CURRENT_VERSION=$(node -p "require('./package.json').version")
npm version "${CURRENT_VERSION}-${VERSION_SUFFIX}" --no-git-tag-version --allow-same-version
env:
VERSION_SUFFIX: ${{ steps.pr.outputs.version_suffix }}

- name: Build and publish PR pre-release
run: |
cd packages/vana-sdk
NODE_OPTIONS="--max-old-space-size=4096" npm run build
npm publish --tag "${DIST_TAG}" --access public
env:
DIST_TAG: ${{ steps.pr.outputs.dist_tag }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

publish-prerelease:
if: github.event_name == 'push'
runs-on: ubuntu-latest
permissions:
contents: read
Expand All @@ -39,8 +111,6 @@ jobs:

- name: Verify npm token
run: |
echo "Token length: ${#NODE_AUTH_TOKEN}"
echo "Token prefix: ${NODE_AUTH_TOKEN:0:4}..."
npm whoami --registry https://registry.npmjs.org/ || echo "npm whoami failed - token may be invalid"
npm view @opendatalabs/vana-sdk version --registry https://registry.npmjs.org/ || echo "Cannot access package"
cat ~/.npmrc 2>/dev/null | sed 's/=.*/=<REDACTED>/' || echo "No .npmrc found"
Expand Down
4 changes: 3 additions & 1 deletion packages/vana-sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,13 @@
"build:node": "tsup --config tsup.config.ts",
"build:browser": "tsup --config tsup-browser.config.ts",
"build:entries": "tsx scripts/bundle-entry-points.ts",
"build": "npm run clean && npm run build:types && npm run build:node && npm run build:browser && npm run build:entries",
"build:fix-esm": "tsx scripts/fix-esm-import-extensions.ts",
"build": "npm run clean && npm run build:types && npm run build:node && npm run build:browser && npm run build:entries && npm run build:fix-esm",
"dev": "npm run build -- --watch",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"typecheck": "tsc -p tsconfig.json",
"validate:package-imports": "npm run build && tsx scripts/validate-package-imports.ts",
"validate:types": "rimraf test-dist && tsc -p tsconfig.build.json --outDir test-dist && test -f test-dist/index.node.d.ts && test -f test-dist/index.browser.d.ts && echo '✅ Type declarations can be generated' && rimraf test-dist || (echo '❌ Failed to generate type declarations' && rimraf test-dist && exit 1)",
"test": "vitest",
"test:verbose": "vitest --reporter=verbose",
Expand Down
92 changes: 92 additions & 0 deletions packages/vana-sdk/scripts/fix-esm-import-extensions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
#!/usr/bin/env tsx
import {
existsSync,
readdirSync,
readFileSync,
statSync,
writeFileSync,
} from "node:fs";
import { dirname, extname, join, relative, resolve } from "node:path";

const distDir = resolve(process.cwd(), "dist");
const specifierPattern =
/(\bfrom\s*["']|import\s*\(\s*["'])(\.{1,2}\/[^"']+)(["'])/g;

function collectJsFiles(dir: string): string[] {
const files: string[] = [];

for (const entry of readdirSync(dir)) {
const path = join(dir, entry);
const stat = statSync(path);

if (stat.isDirectory()) {
files.push(...collectJsFiles(path));
continue;
}

if (path.endsWith(".js")) {
files.push(path);
}
}

return files;
}

function hasExplicitTarget(specifier: string): boolean {
const extension = extname(specifier);
return extension !== "";
}

function resolveEsmTarget(importer: string, specifier: string): string | null {
if (hasExplicitTarget(specifier)) {
return null;
}

const basePath = resolve(dirname(importer), specifier);
if (existsSync(`${basePath}.js`)) {
return `${specifier}.js`;
}

if (existsSync(join(basePath, "index.js"))) {
return `${specifier}/index.js`;
}

return null;
}

if (!existsSync(distDir)) {
throw new Error(
"dist directory does not exist. Run the build before fixing ESM imports.",
);
}

let filesChanged = 0;
let importsChanged = 0;

for (const file of collectJsFiles(distDir)) {
const original = readFileSync(file, "utf8");
let fileImportsChanged = 0;

const updated = original.replace(
specifierPattern,
(match, prefix: string, specifier: string, suffix: string) => {
const target = resolveEsmTarget(file, specifier);
if (!target) {
return match;
}

fileImportsChanged += 1;
return `${prefix}${target}${suffix}`;
},
);

if (updated !== original) {
writeFileSync(file, updated);
filesChanged += 1;
importsChanged += fileImportsChanged;
}
}

console.log(
`Fixed ${importsChanged} ESM import specifier${importsChanged === 1 ? "" : "s"} in ${filesChanged} file${filesChanged === 1 ? "" : "s"} under ${relative(process.cwd(), distDir)}`,
);
55 changes: 55 additions & 0 deletions packages/vana-sdk/scripts/validate-package-imports.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
#!/usr/bin/env tsx
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { execFileSync } from "node:child_process";

const imports = [
"@opendatalabs/vana-sdk/protocol/personal-server-registration",
"@opendatalabs/vana-sdk/protocol/personal-server-lite-owner-binding",
"@opendatalabs/vana-sdk/account/personal-server-registration",
"@opendatalabs/vana-sdk/account/personal-server-lite-owner-binding",
"@opendatalabs/vana-sdk/browser",
];

function run(command: string, args: string[], cwd: string): string {
return execFileSync(command, args, {
cwd,
encoding: "utf8",
stdio: ["ignore", "pipe", "pipe"],
});
}

const tempDir = mkdtempSync(join(tmpdir(), "vana-sdk-package-imports-"));

try {
const packOutput = run(
"npm",
["pack", "--pack-destination", tempDir],
process.cwd(),
);
const tarball = join(tempDir, packOutput.trim().split(/\r?\n/).at(-1) ?? "");
const consumerDir = join(tempDir, "consumer");

run("npm", ["init", "-y"], tempDir);
run(
"npm",
["install", "--prefix", consumerDir, "--no-audit", "--no-fund", tarball],
tempDir,
);

for (const specifier of imports) {
run(
"node",
[
"--input-type=module",
"-e",
`await import(${JSON.stringify(specifier)})`,
],
consumerDir,
);
console.log(`✓ ${specifier}`);
}
} finally {
rmSync(tempDir, { recursive: true, force: true });
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { describe, expect, it, vi } from "vitest";
import type { Address } from "viem";
import { signPersonalServerLiteOwnerBindingWithAccountClient } from "./personal-server-lite-owner-binding";
import type { AccountPersonalServerLiteOwnerBindingError } from "./personal-server-lite-owner-binding";

const OWNER_ADDRESS = "0x2ab394e4be7c43ac360d226a31e1c90bc01aafa1" as Address;
const LOWER_OWNER_ADDRESS = "0x2ab394e4be7c43ac360d226a31e1c90bc01aafa1";
const SIGNATURE = `0x${"cc".repeat(65)}` as const;

describe("Account PS Lite owner-binding integration", () => {
it("gets the Account wallet and signs the PS Lite owner-binding message", async () => {
const client = {
getAddress: vi.fn().mockResolvedValue(OWNER_ADDRESS),
signMessage: vi.fn().mockResolvedValue(SIGNATURE),
};

const result = await signPersonalServerLiteOwnerBindingWithAccountClient({
client,
});

const message = `vana.account.v1:ps-lite-owner:${LOWER_OWNER_ADDRESS}`;
expect(client.signMessage).toHaveBeenCalledWith({ message });
expect(result).toEqual({
signature: SIGNATURE,
signerAddress: OWNER_ADDRESS,
message,
purpose: "ps-lite-owner",
});
});

it("uses a typed Account error when Account has no wallet", async () => {
const client = {
getAddress: vi.fn().mockResolvedValue(null),
signMessage: vi.fn(),
};

await expect(
signPersonalServerLiteOwnerBindingWithAccountClient({ client }),
).rejects.toMatchObject({
name: "AccountPersonalServerLiteOwnerBindingError",
code: "account_address_required",
} satisfies Partial<AccountPersonalServerLiteOwnerBindingError>);
});

it("preserves wallet rejection codes", async () => {
const rejection = Object.assign(new Error("User rejected request"), {
code: 4001,
});
const client = {
getAddress: vi.fn().mockResolvedValue(OWNER_ADDRESS),
signMessage: vi.fn().mockRejectedValue(rejection),
};

await expect(
signPersonalServerLiteOwnerBindingWithAccountClient({ client }),
).rejects.toMatchObject({
name: "AccountPersonalServerLiteOwnerBindingError",
code: 4001,
message: "User rejected request",
details: rejection,
} satisfies Partial<AccountPersonalServerLiteOwnerBindingError>);
});
});
Loading
Loading