> For the complete documentation index, see [llms.txt](https://govplane.gitbook.io/docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://govplane.gitbook.io/docs/documentation/sdk-for-javascript-node.js/migrating-from-1.x.md).

# Migrating from 1.x

The 2.x SDK changes three things that matter: **how you construct a client**, **which signature algorithms are accepted**, and **which bytes a signature covers**.

The third is the reason for the major version. If you were verifying signatures in 1.x, read Why signatures changed before anything else.

### Constructing a client

#### Remote

```diff
- const client = new RuntimeClient({
-   baseUrl, runtimeKey, orgId, projectId, env,
-   signing: { algorithm: 'ECDSA_SHA_256', publicKey },
-   pollMs: 5000,
- });
- await client.start();
+ const client = await createRemoteClient({
+   baseUrl, runtimeKey, orgId, projectId, env,
+   verify: { algorithm: 'ECDSA_SHA_256', publicKey },
+   poll: { intervalMs: 5000 },
+ });
```

The first fetch happens **during construction**, so there is no `start()`, no `warmStart()`, and no window in which the client exists without a bundle. `hasValidBundle` is gone because the state it reported cannot occur.

#### Local — formerly isolated mode

```diff
- const client = new RuntimeClient({
-   baseUrl, runtimeKey,
-   isolation: {
-     safeBundlePath: '/etc/govplane/safe-bundle.json',
-     publicKey,
-     expectedOrgId: 'org_…',
-     expectedProjectId: 'proj_…',
-     expectedEnv: 'prod',
-     triggerFilePath: '/etc/govplane/isolated.mode',
-   },
- });
+ const client = await createLocalClient({
+   bundlePath: '/etc/govplane/policy-bundle.json',
+   verify: { algorithm: 'ECDSA_SHA_256', publicKey },
+ });
```

Note what is gone: `baseUrl`, `runtimeKey`, `expectedOrgId`, `expectedProjectId`, `expectedEnv`. **A local bundle needs none of them.**

{% hint style="info" %}
**Isolated mode is no longer a mode.** It was a fallback you switched into when the control plane was unreachable. A local bundle is now simply a source you choose — and it *is* the old "run from the local copy" behaviour, permanently, with no trigger file to manage.
{% endhint %}

### Renamed and removed

| 1.x                                         | 2.x                                                   |
| ------------------------------------------- | ----------------------------------------------------- |
| `new RuntimeClient({ … })`                  | `createRemoteClient({ … })`                           |
| `isolation: { … }`                          | `createLocalClient({ … })`                            |
| `IsolationModeConfig`                       | `LocalClientConfig`                                   |
| `SafeBundle`                                | `RuntimeBundle`                                       |
| `SafeBundleSignature`                       | `BundleSignature`                                     |
| `SafeBundleValidationError`                 | `BundleValidationError` (old name still resolves)     |
| `signing:`                                  | `verify:`                                             |
| `safeBundlePath`                            | `bundlePath`                                          |
| `pollMs`, `backoffBaseMs`, …                | `poll: { intervalMs, backoffBaseMs, … }`              |
| `start()`, `warmStart()`                    | removed — construction loads the bundle               |
| `stop()`                                    | `close()`                                             |
| `triggerFilePath`, `GOVPLANE_MODE=isolated` | removed — use a local client                          |
| `canonicalJson()`                           | `canonicalPayload()` — **different bytes**, see below |

`createPolicyEngine`, `formatTrace`, all decision and trace types, and every `engine` option are **unchanged**. Code that only calls `evaluate()` needs no edits.

### Why signatures changed

In 1.x the SDK verified signatures over bytes **nobody was signing**.

Both the CLI and the control plane sign a *projection* of the bundle:

```
{ schemaVersion, orgId, projectId, env,
  policies[] → { policyKey, activeVersion, defaults, rules } }
```

deep key-sorted, compact UTF-8. `generatedAt`, `bundleVersion`, `checksum` and `signature` are outside it — which is what lets a bundle be re-versioned without invalidating its signature.

The 1.x SDK had two verifiers and neither used that projection: one hashed the whole response body, the other the whole document minus `signature`. Both are replaced by a single path over the correct bytes, checked against fixtures shared with the CLI and the control plane.

{% hint style="danger" %}
**If signature verification appeared to work in 1.x, it was doing something other than what you assumed. If it appeared to be broken, it was.** Either way, 2.x verifies what is actually signed — so a signature that "worked" before may now fail. Re-sign with the current CLI.
{% endhint %}

#### Ed25519 is gone

The 1.x safe-bundle verifier accepted Ed25519 and nothing else. Nothing has ever produced an Ed25519 bundle signature, and AWS KMS does not offer Ed25519 at all — so it can never be part of a validated signing path.

```bash
govplane sign --signing-algorithm ECDSA_SHA_256
```

#### HMAC is local-only

A cloud bundle must now be ECDSA-signed. Verifying HMAC means holding the signing secret, and a cloud signature comes from a key that never leaves a FIPS 140-3 Level 3 HSM. HMAC remains fully supported for bundles you sign yourself with the CLI.

### Behaviour changes worth knowing

**Unsigned bundles are refused by default.** 1.x isolated mode required a signature; the remote path verified only when `signing` was configured. 2.x is consistent: no verification key means no evaluation, unless you pass `allowUnsigned: true`.

**A signed bundle with no configured key is also refused.** Silently treating it as verified would be the most dangerous possible default.

**Context validation is off unless you configure a policy.** 1.x defaulted `validateContext` to `true` against a seven-key sample allow-list that rejects the keys real bundles read. If you set `validateContext: false` to work around that, drop it. If you want validation, pass a `contextPolicy` — see Context validation.

**`orgId` and `projectId` are optional for local bundles.** They address a bundle within the control plane. A bundle built by `govplane build` has no scope, and requiring one was the last thing tying local evaluation to a Govplane account.

**A local bundle is read once unless you pass `watch`.** There is no implicit file watching.

**Node.js 20.19 is the floor**, and the package ships a single ESM build that `require()` also loads. See ESM and CommonJS.

### New in 2.1

Additive — nothing in 2.0 changed.

```ts
// Shorter return shapes
govplane.evaluate({ target, context }, { asBoolean: true });   // boolean
govplane.evaluate({ target, context }, { asBinary: true });    // 1 | 0
govplane.evaluate({ target, context }, { effectOnly: true });  // 'allow' | 'deny' | …

// Evaluate one named rule
govplane.evaluate({ ruleId: 'deny-large', policyKey: 'refund-control', context });
```

See Evaluating Decisions.

### Checklist

1. Replace `new RuntimeClient(…)` with `createRemoteClient(…)`, `await` it, drop `start()` / `warmStart()`.
2. Replace `isolation: {…}` with a separate `createLocalClient(…)`.
3. Rename `signing:` to `verify:`, `safeBundlePath` to `bundlePath`, `stop()` to `close()`.
4. Move poll and backoff options under `poll: {}`.
5. Re-sign Ed25519 bundles with `ECDSA_SHA_256`.
6. Decide `allowUnsigned` for any bundle you do not verify.
7. Drop `validateContext: false` if you set it only to disable the sample policy.
8. Confirm Node.js is 20.19 or later.
9. Run your policy tests. A signature that "worked" in 1.x and now fails was not signed over the bytes 1.x was checking — re-sign it.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://govplane.gitbook.io/docs/documentation/sdk-for-javascript-node.js/migrating-from-1.x.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
