@thejoseki/clawform 2.3.0 → 2.3.2

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 (48) hide show
  1. package/README.md +141 -120
  2. package/bin/clawform.js +161 -151
  3. package/package.json +65 -65
  4. package/src/aws/catalog.js +116 -116
  5. package/src/aws/changeset.js +231 -231
  6. package/src/aws/drift.js +59 -59
  7. package/src/aws/exports.js +213 -213
  8. package/src/aws/inventory.js +156 -156
  9. package/src/commands/activate.js +34 -2
  10. package/src/commands/ci-init.js +393 -212
  11. package/src/commands/cost-report.js +163 -163
  12. package/src/commands/deactivate.js +9 -2
  13. package/src/commands/deploy.js +413 -413
  14. package/src/commands/diff.js +39 -39
  15. package/src/commands/drift.js +35 -35
  16. package/src/commands/init-plugin.js +6 -6
  17. package/src/commands/init.js +360 -360
  18. package/src/commands/rollback.js +126 -126
  19. package/src/commands/status.js +147 -147
  20. package/src/commands/sync.js +50 -50
  21. package/src/commands/update.js +24 -0
  22. package/src/commands/validate.js +349 -349
  23. package/src/hooks/block-dangerous.js +62 -62
  24. package/src/hooks/cfn-lint-after-edit-eval.js +30 -30
  25. package/src/hooks/cfn-lint-after-edit.js +81 -81
  26. package/src/hooks/check-catalog-fresh-eval.js +63 -63
  27. package/src/hooks/check-catalog-fresh.js +33 -33
  28. package/src/hooks/session-context-eval.js +81 -81
  29. package/src/hooks/session-context.js +41 -41
  30. package/src/hooks/subagent-context-eval.js +44 -44
  31. package/src/hooks/subagent-context.js +48 -48
  32. package/src/lib/audit-synthesis.js +24 -24
  33. package/src/lib/aws-client.js +15 -15
  34. package/src/lib/cfn-yaml.js +92 -92
  35. package/src/lib/config.js +76 -76
  36. package/src/lib/constants.js +85 -85
  37. package/src/lib/defaults.js +16 -16
  38. package/src/lib/install-workflow.js +28 -28
  39. package/src/lib/kit-source.js +43 -5
  40. package/src/lib/license-client.js +84 -84
  41. package/src/lib/license-config.js +162 -162
  42. package/src/lib/license-store.js +43 -8
  43. package/src/lib/license.js +15 -2
  44. package/src/lib/lint-overrides.js +226 -226
  45. package/src/lib/logger.js +16 -16
  46. package/src/lib/project-config.js +145 -145
  47. package/src/lib/providers/polar.js +215 -215
  48. package/src/lib/session-state.js +91 -91
@@ -9,12 +9,20 @@
9
9
  // repaired with no network, which matters because `init plugin` is not
10
10
  // something a buyer should have to be online to re-run.
11
11
 
12
- import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
12
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, statSync as fsStatSync } from 'node:fs';
13
13
  import { join } from 'node:path';
14
14
  import { unpackFiles } from './payload.js';
15
+ import { logger } from './logger.js';
15
16
 
16
17
  export const KIT_CACHE_FILENAME = 'kit.bin';
17
18
 
19
+ // Backstop against a permanently-frozen cache: the kit is served by a pipeline
20
+ // SEPARATE from npm, so a version-stamped filename cannot catch "kit changed,
21
+ // CLI did not". After this window a re-run re-checks the service (and still
22
+ // falls back to the cached copy if the service is down). `--refresh` is the
23
+ // deterministic path; this only bounds how long a silent staleness can last.
24
+ export const KIT_CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24h
25
+
18
26
  // Said on every service failure. Someone hitting this may have a stack stuck
19
27
  // mid-update, and the first thing they need to know is that the emergency path
20
28
  // is not what just broke. Those two commands are never licence-gated, and they
@@ -46,11 +54,41 @@ async function detailOf(resp) {
46
54
 
47
55
  /**
48
56
  * Resolve the content kit, from cache or from the service.
57
+ *
58
+ * A FRESH cache (younger than ttlMs, and no `refresh`) is served with no
59
+ * network — the common case, and what keeps `init plugin` offline-safe. A cold,
60
+ * stale, or `refresh`-forced call goes to the service; if that fails with an
61
+ * OUTAGE (network / 5xx / 429) but a cache exists, the cache is served with a
62
+ * warning rather than breaking the run. A REFUSAL (403/4xx) still throws — it is
63
+ * a deliberate "this licence cannot have the kit", not a blip to paper over.
64
+ *
49
65
  * @returns {Promise<Array<{path: string, content: string}>>}
50
66
  */
51
- export async function obtainKit({ licenseKey, serviceUrl, cacheDir, fetch: fetchImpl = fetch }) {
67
+ export async function obtainKit({
68
+ licenseKey, serviceUrl, cacheDir,
69
+ fetch: fetchImpl = fetch,
70
+ refresh = false,
71
+ ttlMs = KIT_CACHE_TTL_MS,
72
+ now = Date.now,
73
+ statSync = fsStatSync,
74
+ warn = (m) => logger.warn(m),
75
+ }) {
52
76
  const cachePath = join(cacheDir, KIT_CACHE_FILENAME);
53
- if (existsSync(cachePath)) return unpackFiles(readFileSync(cachePath));
77
+ const cached = existsSync(cachePath);
78
+ const fresh = cached && !refresh && (now() - statSync(cachePath).mtimeMs) < ttlMs;
79
+
80
+ if (fresh) return unpackFiles(readFileSync(cachePath));
81
+
82
+ // Serve the cache on an outage rather than breaking a re-run — but never on a
83
+ // deliberate refusal (below).
84
+ const fallbackOrThrow = (err) => {
85
+ if (cached) {
86
+ warn(`Could not refresh the plugin kit (${err.reason}) — using the cached copy. `
87
+ + 'Re-run `clawform init plugin --refresh` once the service is reachable to pull the latest skills.');
88
+ return unpackFiles(readFileSync(cachePath));
89
+ }
90
+ throw outage(err.reason);
91
+ };
54
92
 
55
93
  let resp;
56
94
  try {
@@ -60,12 +98,12 @@ export async function obtainKit({ licenseKey, serviceUrl, cacheDir, fetch: fetch
60
98
  body: JSON.stringify({ license_key: licenseKey }),
61
99
  });
62
100
  } catch (err) {
63
- throw outage(err?.message ?? 'network error');
101
+ return fallbackOrThrow({ reason: err?.message ?? 'network error' });
64
102
  }
65
103
 
66
104
  // A throttle or a 5xx is the service failing, not the licence being judged —
67
105
  // the same distinction the vendor client draws, and for the same reason.
68
- if (resp.status === 429 || resp.status >= 500) throw outage(`HTTP ${resp.status}`);
106
+ if (resp.status === 429 || resp.status >= 500) return fallbackOrThrow({ reason: `HTTP ${resp.status}` });
69
107
  if (!resp.ok) throw refusal(await detailOf(resp));
70
108
 
71
109
  const bundle = Buffer.from(await resp.arrayBuffer());
@@ -1,84 +1,84 @@
1
- // Vendor-agnostic license client seam. Every consumer talks to this contract:
2
- //
3
- // activate(key, fingerprint) → { status:'valid', instanceId, holder? }
4
- // | { status:'invalid', reason } // limit_reached | expired | not_found
5
- // | { status:'error', reason } // vendor unreachable
6
- // validate(key, activationId?) → { status:'valid'|'invalid'|'error', reason? }
7
- // deactivate(key, activationId)→ { status:'valid'|'invalid'|'error', reason? }
8
- //
9
- // resolveClient() picks the implementation from the license mode:
10
- // 'off' (default) → a dormant stub (configured:false) — assertLicensed
11
- // passes everything, so the repo's own dev/CI and every
12
- // buyer before launch is unaffected.
13
- // 'sandbox'/'live' → the real Polar adapter (configured:true) against the
14
- // matching base URL.
15
- //
16
- // Flipping the shipped default to a live mode is the single reviewed decision
17
- // that turns enforcement on (see license-config.js).
18
-
19
- import { existsSync } from 'node:fs';
20
- import { join, dirname } from 'node:path';
21
- import { fileURLToPath } from 'node:url';
22
- import { readEnv } from './rename-compat.js';
23
- import { createPolarClient } from './providers/polar.js';
24
- import {
25
- LICENSE_MODE_DEFAULT,
26
- DEV_MARKER,
27
- POLAR_ORGANIZATION_ID,
28
- POLAR_BENEFIT_ID,
29
- POLAR_TRIAL_BENEFIT_ID,
30
- POLAR_BASE_URL,
31
- } from './license-config.js';
32
-
33
- const DORMANT = {
34
- configured: false,
35
- async activate() { return { status: 'error', reason: 'license enforcement is off in this build' }; },
36
- async validate() { return { status: 'error', reason: 'license enforcement is off in this build' }; },
37
- async deactivate() { return { status: 'error', reason: 'license enforcement is off in this build' }; },
38
- };
39
-
40
- // Resolved from THIS module's location, never from process.cwd(): with cwd, a
41
- // buyer running clawform inside any repo that happens to carry the dev marker
42
- // would be handed the dormant client — a bypass needing no edit at all.
43
- const PACKAGE_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
44
-
45
- export function isSourceCheckout(packageRoot = PACKAGE_ROOT) {
46
- return existsSync(join(packageRoot, DEV_MARKER));
47
- }
48
-
49
- // `env` stays POSITIONAL: callers pass an env object, and folding it into an
50
- // options bag would silently reinterpret every existing call as `{env:
51
- // undefined}` — falling back to process.env and passing for the wrong reason.
52
- export function resolveMode(env = process.env, { packageRoot = PACKAGE_ROOT } = {}) {
53
- const fromEnv = readEnv('LICENSE_MODE', env);
54
- // The environment may pick BETWEEN enforcing modes; it can never turn
55
- // enforcement down. Anything else — 'off', a typo, an empty string — is
56
- // ignored rather than treated as a request to disable.
57
- if (fromEnv === 'sandbox' || fromEnv === 'live') return fromEnv;
58
- if (isSourceCheckout(packageRoot)) return 'off';
59
- return LICENSE_MODE_DEFAULT;
60
- }
61
-
62
- // Which benefits are trials, for the mode this install is running in. Resolved
63
- // here rather than in license-config because that is where mode resolution
64
- // already lives, and a second copy of it would be free to disagree.
65
- //
66
- // A dormant install has no trials: nothing to claim, nothing to refuse.
67
- export function resolveTrialBenefitIds({ env = process.env, packageRoot = PACKAGE_ROOT } = {}) {
68
- const mode = resolveMode(env, { packageRoot });
69
- return POLAR_TRIAL_BENEFIT_ID[mode] ?? [];
70
- }
71
-
72
- export function resolveClient({ env = process.env, packageRoot = PACKAGE_ROOT, fetch } = {}) {
73
- const mode = resolveMode(env, { packageRoot });
74
- if (mode !== 'sandbox' && mode !== 'live') return DORMANT;
75
- // Both looked up by the same mode, so they cannot drift apart: a base URL
76
- // for one environment with an organization from the other is the failure
77
- // this shape exists to make impossible.
78
- return createPolarClient({
79
- baseUrl: POLAR_BASE_URL[mode],
80
- organizationId: POLAR_ORGANIZATION_ID[mode],
81
- benefitIds: POLAR_BENEFIT_ID[mode],
82
- ...(fetch ? { fetch } : {}),
83
- });
84
- }
1
+ // Vendor-agnostic license client seam. Every consumer talks to this contract:
2
+ //
3
+ // activate(key, fingerprint) → { status:'valid', instanceId, holder? }
4
+ // | { status:'invalid', reason } // limit_reached | expired | not_found
5
+ // | { status:'error', reason } // vendor unreachable
6
+ // validate(key, activationId?) → { status:'valid'|'invalid'|'error', reason? }
7
+ // deactivate(key, activationId)→ { status:'valid'|'invalid'|'error', reason? }
8
+ //
9
+ // resolveClient() picks the implementation from the license mode:
10
+ // 'off' (default) → a dormant stub (configured:false) — assertLicensed
11
+ // passes everything, so the repo's own dev/CI and every
12
+ // buyer before launch is unaffected.
13
+ // 'sandbox'/'live' → the real Polar adapter (configured:true) against the
14
+ // matching base URL.
15
+ //
16
+ // Flipping the shipped default to a live mode is the single reviewed decision
17
+ // that turns enforcement on (see license-config.js).
18
+
19
+ import { existsSync } from 'node:fs';
20
+ import { join, dirname } from 'node:path';
21
+ import { fileURLToPath } from 'node:url';
22
+ import { readEnv } from './rename-compat.js';
23
+ import { createPolarClient } from './providers/polar.js';
24
+ import {
25
+ LICENSE_MODE_DEFAULT,
26
+ DEV_MARKER,
27
+ POLAR_ORGANIZATION_ID,
28
+ POLAR_BENEFIT_ID,
29
+ POLAR_TRIAL_BENEFIT_ID,
30
+ POLAR_BASE_URL,
31
+ } from './license-config.js';
32
+
33
+ const DORMANT = {
34
+ configured: false,
35
+ async activate() { return { status: 'error', reason: 'license enforcement is off in this build' }; },
36
+ async validate() { return { status: 'error', reason: 'license enforcement is off in this build' }; },
37
+ async deactivate() { return { status: 'error', reason: 'license enforcement is off in this build' }; },
38
+ };
39
+
40
+ // Resolved from THIS module's location, never from process.cwd(): with cwd, a
41
+ // buyer running clawform inside any repo that happens to carry the dev marker
42
+ // would be handed the dormant client — a bypass needing no edit at all.
43
+ const PACKAGE_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
44
+
45
+ export function isSourceCheckout(packageRoot = PACKAGE_ROOT) {
46
+ return existsSync(join(packageRoot, DEV_MARKER));
47
+ }
48
+
49
+ // `env` stays POSITIONAL: callers pass an env object, and folding it into an
50
+ // options bag would silently reinterpret every existing call as `{env:
51
+ // undefined}` — falling back to process.env and passing for the wrong reason.
52
+ export function resolveMode(env = process.env, { packageRoot = PACKAGE_ROOT } = {}) {
53
+ const fromEnv = readEnv('LICENSE_MODE', env);
54
+ // The environment may pick BETWEEN enforcing modes; it can never turn
55
+ // enforcement down. Anything else — 'off', a typo, an empty string — is
56
+ // ignored rather than treated as a request to disable.
57
+ if (fromEnv === 'sandbox' || fromEnv === 'live') return fromEnv;
58
+ if (isSourceCheckout(packageRoot)) return 'off';
59
+ return LICENSE_MODE_DEFAULT;
60
+ }
61
+
62
+ // Which benefits are trials, for the mode this install is running in. Resolved
63
+ // here rather than in license-config because that is where mode resolution
64
+ // already lives, and a second copy of it would be free to disagree.
65
+ //
66
+ // A dormant install has no trials: nothing to claim, nothing to refuse.
67
+ export function resolveTrialBenefitIds({ env = process.env, packageRoot = PACKAGE_ROOT } = {}) {
68
+ const mode = resolveMode(env, { packageRoot });
69
+ return POLAR_TRIAL_BENEFIT_ID[mode] ?? [];
70
+ }
71
+
72
+ export function resolveClient({ env = process.env, packageRoot = PACKAGE_ROOT, fetch } = {}) {
73
+ const mode = resolveMode(env, { packageRoot });
74
+ if (mode !== 'sandbox' && mode !== 'live') return DORMANT;
75
+ // Both looked up by the same mode, so they cannot drift apart: a base URL
76
+ // for one environment with an organization from the other is the failure
77
+ // this shape exists to make impossible.
78
+ return createPolarClient({
79
+ baseUrl: POLAR_BASE_URL[mode],
80
+ organizationId: POLAR_ORGANIZATION_ID[mode],
81
+ benefitIds: POLAR_BENEFIT_ID[mode],
82
+ ...(fetch ? { fetch } : {}),
83
+ });
84
+ }
@@ -1,162 +1,162 @@
1
- // Licensing time policy. Vendor identifiers (org/product IDs) join this file
2
- // once the payout test settles which vendor we ship with — the time policy is
3
- // vendor-independent and the grace/revalidate logic only needs these two.
4
-
5
- // How long a cached "valid" verdict is trusted before the CLI re-checks with
6
- // the vendor in the background. Once per day keeps revocation reasonably fresh
7
- // without turning every command into a network call.
8
- export const REVALIDATE_HOURS = 24;
9
-
10
- // How long a buyer keeps working after the LAST successful validation when the
11
- // vendor cannot be reached (offline dev box, vendor outage). Two weeks: long
12
- // enough to survive a vacation offline, short enough that a revoked-but-
13
- // unreachable key does not run forever.
14
- export const GRACE_DAYS = 14;
15
-
16
- // --- Vendor: Polar -------------------------------------------------------
17
- // The organization id is not a secret — it is a public parameter of every
18
- // customer-portal call, and the API needs no server token besides the buyer's
19
- // own key. Safe to ship in the tarball.
20
- //
21
- // Keyed by mode, and it has to be. Polar's sandbox is a separate system rather
22
- // than a flag on the same one: you sign up for it separately, the organization
23
- // you create there does not exist in production, and its id is different. A
24
- // single id shared across both modes builds a client that talks to
25
- // api.polar.sh while naming a sandbox organization — every activation by a
26
- // real buyer fails, and it fails on the first day of selling rather than in
27
- // any amount of sandbox testing. Paired with POLAR_BASE_URL below so the two
28
- // can only be changed together.
29
- export const POLAR_ORGANIZATION_ID = {
30
- live: '75b98aba-8768-4534-9b76-dd7f16cba3d3',
31
- sandbox: 'c234fc61-20e4-4d6f-b919-0f5823f7984d',
32
- };
33
- export const POLAR_BASE_URL = {
34
- live: 'https://api.polar.sh',
35
- sandbox: 'https://sandbox-api.polar.sh',
36
- };
37
-
38
- // The License Keys benefit this product sells. The organization is not enough
39
- // to identify a key: every product under the same organization validates
40
- // against the same endpoint with the same organization id, so the moment a
41
- // second product sells from here, a key bought for THAT product activates this
42
- // one. A paying customer of one tool silently unlocking another is not a bug
43
- // anyone reports.
44
- //
45
- // Polar returns the benefit on both activate and validate, so the check costs
46
- // nothing and closes it. Keyed by mode for the same reason the organization is
47
- // — a sandbox benefit does not exist in production.
48
- //
49
- // A LIST per mode, one entry per paid tier. Polar carries limit_activations on
50
- // the benefit, not the product, so tiers with different device caps cannot share
51
- // one — the individual seat and the team seat are necessarily separate benefits.
52
- // A key is accepted when it matches ANY entry; the tier it belongs to is Polar's
53
- // business, not the CLI's.
54
- //
55
- // Nothing may appear in both lists: an id reachable from either environment
56
- // would let a sandbox key unlock a live install.
57
- export const POLAR_BENEFIT_ID = {
58
- // [0] individual seat, [1] team seat. Order is not significance — a key is
59
- // accepted on any match — but it is the order the walk tries, so the tier
60
- // that sells most often belongs first.
61
- live: [
62
- 'b85a9271-22ac-45b3-ab0a-b0c93df6b13c',
63
- 'be046860-7be4-43bd-ac8b-cf47d08f77d0',
64
- 'a3fc4e93-9e93-4317-bed5-5b44a5af9e19',
65
- ],
66
- sandbox: [
67
- '69d1ccaa-65c4-48da-ad4a-64961772c17e',
68
- 'a540043d-da17-48ae-bee6-e9a900fbe1cc',
69
- '9c5fe82d-09a7-490c-91e3-47d164603423',
70
- ],
71
- };
72
-
73
- // Which of the benefits above are TRIALS. A trial key validates like any other
74
- // — it has to appear in POLAR_BENEFIT_ID as well — but it additionally claims
75
- // the machine, so one device cannot start a second trial under a new email.
76
- //
77
- // Listed separately rather than encoded in the entries above so phase 01's
78
- // shape and its tests stay untouched. A test pins that every id here also
79
- // appears in POLAR_BENEFIT_ID: an id in only one list is a trial key nobody can
80
- // activate, or a paid key that silently burns a device claim.
81
- //
82
- // The licence-key benefit on each trial product must be VISIBLE in Polar: the
83
- // buyer has to read the key out of the portal or the confirmation email to type
84
- // it into `clawform activate`. A hidden benefit is granted and unusable.
85
- export const POLAR_TRIAL_BENEFIT_ID = {
86
- live: ['a3fc4e93-9e93-4317-bed5-5b44a5af9e19'],
87
- sandbox: ['9c5fe82d-09a7-490c-91e3-47d164603423'],
88
- };
89
-
90
- // --- Content kit delivery -----------------------------------------------
91
- // The plugin content (skills/commands/agents/rules/templates) is NOT in the npm
92
- // tarball. It is fetched by `clawform init plugin` from this service, which
93
- // checks the licence with the vendor before serving anything.
94
- //
95
- // The previous model shipped the content as an encrypted blob with its key
96
- // beside it in the same tarball — which protected nothing: `npm i` handed a
97
- // non-buyer both halves. There is no key here now because the CLI never
98
- // decrypts; the bundle lives in a private bucket and travels over TLS.
99
- //
100
- // Not keyed by licence mode. There is one kit, and which licences may have it
101
- // is the service's judgement, not a build-time constant.
102
- export const KIT_SERVICE_URL = 'https://kit.thejoseki.com';
103
-
104
- // Written by scripts/pack-payload.js for upload, and cached under the vendor
105
- // home after a successful fetch. Never present in the published package.
106
- export const KIT_FILENAME = 'kit.bin';
107
-
108
- // Signs the local licence record. The gate trusts an 'active' record with a
109
- // fresh timestamp without calling the vendor, so an unsigned cache would make
110
- // `echo '{"status":"active",…}' > ~/.clawform/license.json` a complete bypass —
111
- // and a shareable one-liner at that. Signing raises it to "extract this secret
112
- // from the CLI first", the same bar as the dev marker.
113
- //
114
- // Same 1a honesty as PAYLOAD_KEY_HEX: the secret ships with the code, so a
115
- // determined attacker can still forge. The goal is killing the one-liner class.
116
- // Distinct from the payload key on purpose — keys with different jobs.
117
- //
118
- // Rotates alongside the payload key, and for the same reason: a release that
119
- // published this value hands every later release a forged 'active' record.
120
- // The cost is that records signed with the old secret read as unlicensed, so a
121
- // buyer re-activates — cheap now, and it only gets dearer with every seat sold.
122
- export const LICENSE_RECORD_SECRET_HEX = 'c4179ff7c3e2ebd223fdb2653ed1163f5b8cb89c7900af2a88f186fbb55818e5';
123
-
124
- // Tolerance for a machine clock running slightly ahead of the vendor's. Beyond
125
- // it, a future `lastValidatedAt` is treated as needing revalidation rather than
126
- // as an eternally fresh verdict.
127
- export const CLOCK_SKEW_HOURS = 1;
128
-
129
- // How a build tells whether it is a source checkout or an installed package.
130
- // This file is the payload build script — `package.json` `files` never ships
131
- // `scripts/`, so it is present in a clone and absent in `npm i -g @thejoseki/clawform`.
132
- //
133
- // Dormancy is granted by the install SHAPE, not by anything a buyer can set:
134
- // a `CLAWFORM_LICENSE_MODE=off` that disabled the gate would be a
135
- // copy-pasteable one-liner, the one bypass class that actually spreads.
136
- // Faking dormancy now means creating this file inside the installed package —
137
- // an edit, not a one-liner.
138
- //
139
- // ⚠️ Adding `scripts/` to `package.json` `files` would ship the marker and
140
- // silently disable enforcement for everyone. A packaging test asserts it stays
141
- // out of the tarball.
142
- export const DEV_MARKER = 'scripts/pack-payload.js';
143
-
144
- // The master switch, and the single decision that turns enforcement on.
145
- //
146
- // Held at 'off' through development, because enforcing before a key could be
147
- // bought would have locked out every user with no way back in. It moved to
148
- // 'live' only once all three preconditions were verified against the
149
- // production vendor rather than assumed:
150
- //
151
- // - a Polar product exists with a License Keys benefit, activation limit 3
152
- // - a real purchased key drives activate / validate / deactivate, the
153
- // three-device cap refuses a fourth machine, and deactivate demonstrably
154
- // frees a slot (proved by re-activating, not by the return code)
155
- // - the sandbox and production environments reject each other's keys, which
156
- // is what makes the per-mode organization and benefit ids load-bearing
157
- //
158
- // A source checkout stays dormant regardless: that exemption comes from the
159
- // dev marker's presence, not from this value, so contributors need no key.
160
- // Overridable per run via CLAWFORM_LICENSE_MODE, which can raise enforcement
161
- // but never lower it.
162
- export const LICENSE_MODE_DEFAULT = 'live';
1
+ // Licensing time policy. Vendor identifiers (org/product IDs) join this file
2
+ // once the payout test settles which vendor we ship with — the time policy is
3
+ // vendor-independent and the grace/revalidate logic only needs these two.
4
+
5
+ // How long a cached "valid" verdict is trusted before the CLI re-checks with
6
+ // the vendor in the background. Once per day keeps revocation reasonably fresh
7
+ // without turning every command into a network call.
8
+ export const REVALIDATE_HOURS = 24;
9
+
10
+ // How long a buyer keeps working after the LAST successful validation when the
11
+ // vendor cannot be reached (offline dev box, vendor outage). Two weeks: long
12
+ // enough to survive a vacation offline, short enough that a revoked-but-
13
+ // unreachable key does not run forever.
14
+ export const GRACE_DAYS = 14;
15
+
16
+ // --- Vendor: Polar -------------------------------------------------------
17
+ // The organization id is not a secret — it is a public parameter of every
18
+ // customer-portal call, and the API needs no server token besides the buyer's
19
+ // own key. Safe to ship in the tarball.
20
+ //
21
+ // Keyed by mode, and it has to be. Polar's sandbox is a separate system rather
22
+ // than a flag on the same one: you sign up for it separately, the organization
23
+ // you create there does not exist in production, and its id is different. A
24
+ // single id shared across both modes builds a client that talks to
25
+ // api.polar.sh while naming a sandbox organization — every activation by a
26
+ // real buyer fails, and it fails on the first day of selling rather than in
27
+ // any amount of sandbox testing. Paired with POLAR_BASE_URL below so the two
28
+ // can only be changed together.
29
+ export const POLAR_ORGANIZATION_ID = {
30
+ live: '75b98aba-8768-4534-9b76-dd7f16cba3d3',
31
+ sandbox: 'c234fc61-20e4-4d6f-b919-0f5823f7984d',
32
+ };
33
+ export const POLAR_BASE_URL = {
34
+ live: 'https://api.polar.sh',
35
+ sandbox: 'https://sandbox-api.polar.sh',
36
+ };
37
+
38
+ // The License Keys benefit this product sells. The organization is not enough
39
+ // to identify a key: every product under the same organization validates
40
+ // against the same endpoint with the same organization id, so the moment a
41
+ // second product sells from here, a key bought for THAT product activates this
42
+ // one. A paying customer of one tool silently unlocking another is not a bug
43
+ // anyone reports.
44
+ //
45
+ // Polar returns the benefit on both activate and validate, so the check costs
46
+ // nothing and closes it. Keyed by mode for the same reason the organization is
47
+ // — a sandbox benefit does not exist in production.
48
+ //
49
+ // A LIST per mode, one entry per paid tier. Polar carries limit_activations on
50
+ // the benefit, not the product, so tiers with different device caps cannot share
51
+ // one — the individual seat and the team seat are necessarily separate benefits.
52
+ // A key is accepted when it matches ANY entry; the tier it belongs to is Polar's
53
+ // business, not the CLI's.
54
+ //
55
+ // Nothing may appear in both lists: an id reachable from either environment
56
+ // would let a sandbox key unlock a live install.
57
+ export const POLAR_BENEFIT_ID = {
58
+ // [0] individual seat, [1] team seat. Order is not significance — a key is
59
+ // accepted on any match — but it is the order the walk tries, so the tier
60
+ // that sells most often belongs first.
61
+ live: [
62
+ 'b85a9271-22ac-45b3-ab0a-b0c93df6b13c',
63
+ 'be046860-7be4-43bd-ac8b-cf47d08f77d0',
64
+ 'a3fc4e93-9e93-4317-bed5-5b44a5af9e19',
65
+ ],
66
+ sandbox: [
67
+ '69d1ccaa-65c4-48da-ad4a-64961772c17e',
68
+ 'a540043d-da17-48ae-bee6-e9a900fbe1cc',
69
+ '9c5fe82d-09a7-490c-91e3-47d164603423',
70
+ ],
71
+ };
72
+
73
+ // Which of the benefits above are TRIALS. A trial key validates like any other
74
+ // — it has to appear in POLAR_BENEFIT_ID as well — but it additionally claims
75
+ // the machine, so one device cannot start a second trial under a new email.
76
+ //
77
+ // Listed separately rather than encoded in the entries above so phase 01's
78
+ // shape and its tests stay untouched. A test pins that every id here also
79
+ // appears in POLAR_BENEFIT_ID: an id in only one list is a trial key nobody can
80
+ // activate, or a paid key that silently burns a device claim.
81
+ //
82
+ // The licence-key benefit on each trial product must be VISIBLE in Polar: the
83
+ // buyer has to read the key out of the portal or the confirmation email to type
84
+ // it into `clawform activate`. A hidden benefit is granted and unusable.
85
+ export const POLAR_TRIAL_BENEFIT_ID = {
86
+ live: ['a3fc4e93-9e93-4317-bed5-5b44a5af9e19'],
87
+ sandbox: ['9c5fe82d-09a7-490c-91e3-47d164603423'],
88
+ };
89
+
90
+ // --- Content kit delivery -----------------------------------------------
91
+ // The plugin content (skills/commands/agents/rules/templates) is NOT in the npm
92
+ // tarball. It is fetched by `clawform init plugin` from this service, which
93
+ // checks the licence with the vendor before serving anything.
94
+ //
95
+ // The previous model shipped the content as an encrypted blob with its key
96
+ // beside it in the same tarball — which protected nothing: `npm i` handed a
97
+ // non-buyer both halves. There is no key here now because the CLI never
98
+ // decrypts; the bundle lives in a private bucket and travels over TLS.
99
+ //
100
+ // Not keyed by licence mode. There is one kit, and which licences may have it
101
+ // is the service's judgement, not a build-time constant.
102
+ export const KIT_SERVICE_URL = 'https://kit.thejoseki.com';
103
+
104
+ // Written by scripts/pack-payload.js for upload, and cached under the vendor
105
+ // home after a successful fetch. Never present in the published package.
106
+ export const KIT_FILENAME = 'kit.bin';
107
+
108
+ // Signs the local licence record. The gate trusts an 'active' record with a
109
+ // fresh timestamp without calling the vendor, so an unsigned cache would make
110
+ // `echo '{"status":"active",…}' > ~/.clawform/license.json` a complete bypass —
111
+ // and a shareable one-liner at that. Signing raises it to "extract this secret
112
+ // from the CLI first", the same bar as the dev marker.
113
+ //
114
+ // Same 1a honesty as PAYLOAD_KEY_HEX: the secret ships with the code, so a
115
+ // determined attacker can still forge. The goal is killing the one-liner class.
116
+ // Distinct from the payload key on purpose — keys with different jobs.
117
+ //
118
+ // Rotates alongside the payload key, and for the same reason: a release that
119
+ // published this value hands every later release a forged 'active' record.
120
+ // The cost is that records signed with the old secret read as unlicensed, so a
121
+ // buyer re-activates — cheap now, and it only gets dearer with every seat sold.
122
+ export const LICENSE_RECORD_SECRET_HEX = 'c4179ff7c3e2ebd223fdb2653ed1163f5b8cb89c7900af2a88f186fbb55818e5';
123
+
124
+ // Tolerance for a machine clock running slightly ahead of the vendor's. Beyond
125
+ // it, a future `lastValidatedAt` is treated as needing revalidation rather than
126
+ // as an eternally fresh verdict.
127
+ export const CLOCK_SKEW_HOURS = 1;
128
+
129
+ // How a build tells whether it is a source checkout or an installed package.
130
+ // This file is the payload build script — `package.json` `files` never ships
131
+ // `scripts/`, so it is present in a clone and absent in `npm i -g @thejoseki/clawform`.
132
+ //
133
+ // Dormancy is granted by the install SHAPE, not by anything a buyer can set:
134
+ // a `CLAWFORM_LICENSE_MODE=off` that disabled the gate would be a
135
+ // copy-pasteable one-liner, the one bypass class that actually spreads.
136
+ // Faking dormancy now means creating this file inside the installed package —
137
+ // an edit, not a one-liner.
138
+ //
139
+ // ⚠️ Adding `scripts/` to `package.json` `files` would ship the marker and
140
+ // silently disable enforcement for everyone. A packaging test asserts it stays
141
+ // out of the tarball.
142
+ export const DEV_MARKER = 'scripts/pack-payload.js';
143
+
144
+ // The master switch, and the single decision that turns enforcement on.
145
+ //
146
+ // Held at 'off' through development, because enforcing before a key could be
147
+ // bought would have locked out every user with no way back in. It moved to
148
+ // 'live' only once all three preconditions were verified against the
149
+ // production vendor rather than assumed:
150
+ //
151
+ // - a Polar product exists with a License Keys benefit, activation limit 3
152
+ // - a real purchased key drives activate / validate / deactivate, the
153
+ // three-device cap refuses a fourth machine, and deactivate demonstrably
154
+ // frees a slot (proved by re-activating, not by the return code)
155
+ // - the sandbox and production environments reject each other's keys, which
156
+ // is what makes the per-mode organization and benefit ids load-bearing
157
+ //
158
+ // A source checkout stays dormant regardless: that exemption comes from the
159
+ // dev marker's presence, not from this value, so contributors need no key.
160
+ // Overridable per run via CLAWFORM_LICENSE_MODE, which can raise enforcement
161
+ // but never lower it.
162
+ export const LICENSE_MODE_DEFAULT = 'live';