@forgedevv/shared 0.1.0 → 0.3.0

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,14 @@
1
+ # Changelog — @forge/shared
2
+
3
+ All notable changes to `@forge/shared`, published on npm as `@forgedevv/shared` together with
4
+ `@forgedevv/core` (same version). Versions follow semantic versioning. Changes before 0.3.0 are
5
+ listed in the `@forge/core` changelog.
6
+
7
+ ## 0.3.0 — 2026-10-17
8
+
9
+ ### Changed (Jupiter fixes)
10
+
11
+ - `FEES.jupiterIntegratorFeeBps` is 50 bps, was 30. It is the Jupiter Plugin `referralFee` after
12
+ graduation; Jupiter Ultra refuses any value outside 50..255 bps (checked by `@forge/core`
13
+ `assertJupiterReferralFeeBps`), and keeps 20% of it. `FEES.platformFeeBps` (30 bps, FORGE's own
14
+ buy button) is unchanged.
package/dist/api.d.ts CHANGED
@@ -97,6 +97,10 @@ export declare const WEB_API_ROUTES: {
97
97
  readonly method: "POST";
98
98
  readonly path: "/api/launchpads/:id/reactivate";
99
99
  };
100
+ readonly launchpadRetry: {
101
+ readonly method: "POST";
102
+ readonly path: "/api/launchpads/:id/retry";
103
+ };
100
104
  readonly rpc: {
101
105
  readonly method: "POST";
102
106
  readonly path: "/api/rpc";
@@ -145,6 +149,7 @@ export declare const GatingResponse: z.ZodObject<{
145
149
  required: z.ZodString;
146
150
  balance: z.ZodString;
147
151
  enabled: z.ZodBoolean;
152
+ decimals: z.ZodOptional<z.ZodNumber>;
148
153
  }, z.core.$strip>;
149
154
  export type GatingResponse = z.infer<typeof GatingResponse>;
150
155
  export declare const FlagsResponse: z.ZodObject<{
@@ -206,6 +211,10 @@ export declare const LaunchpadModificationResponse: z.ZodObject<{
206
211
  jobId: z.ZodUUID;
207
212
  }, z.core.$strip>;
208
213
  export type LaunchpadModificationResponse = z.infer<typeof LaunchpadModificationResponse>;
214
+ export declare const LaunchpadRetryResponse: z.ZodObject<{
215
+ jobId: z.ZodUUID;
216
+ }, z.core.$strip>;
217
+ export type LaunchpadRetryResponse = z.infer<typeof LaunchpadRetryResponse>;
209
218
  export declare const PaymentsQuoteRequest: z.ZodObject<{
210
219
  jobId: z.ZodUUID;
211
220
  kind: z.ZodEnum<{
@@ -368,17 +377,43 @@ export declare const RpcResponse: z.ZodObject<{
368
377
  }, z.core.$strip>>;
369
378
  }, z.core.$strip>;
370
379
  export type RpcResponse = z.infer<typeof RpcResponse>;
371
- /** Hex HMAC-SHA256 computed with SIGNER_HMAC_SECRET over `signerSignedPayload(...)`. */
380
+ /** Lowercase hex HMAC-SHA256 computed with SIGNER_HMAC_SECRET over `signerSignedPayload(...)`. */
372
381
  export declare const SIGNER_SIGNATURE_HEADER = "X-Forge-Signature";
373
382
  /** Unix time in seconds, as a decimal string. */
374
383
  export declare const SIGNER_TIMESTAMP_HEADER = "X-Forge-Timestamp";
384
+ /**
385
+ * 16 random bytes as 32 lowercase hex characters, fresh for every request (and every retry). The
386
+ * signer accepts a nonce once within the clock-skew window.
387
+ */
388
+ export declare const SIGNER_NONCE_HEADER = "X-Forge-Nonce";
375
389
  /** Requests whose timestamp differs from the signer clock by more than this are rejected. */
376
390
  export declare const SIGNER_MAX_CLOCK_SKEW_SECONDS = 60;
391
+ /** First line of the signed string. Any change to its layout gets a new tag. */
392
+ export declare const SIGNER_SIGNATURE_VERSION = "forge-signer-v2";
393
+ /** The request fields covered by the signer HMAC. */
394
+ export interface SignerSignedFields {
395
+ /** HTTP method, uppercase (`POST`). */
396
+ method: string;
397
+ /** Request target exactly as sent: the path plus the query string (`/v1/configs?a=1`). */
398
+ path: string;
399
+ /** Same value as the timestamp header. */
400
+ timestamp: string;
401
+ /** Same value as the nonce header. */
402
+ nonce: string;
403
+ /** Lowercase hex SHA-256 of the raw body bytes (an empty body hashes zero bytes). */
404
+ bodySha256: string;
405
+ }
377
406
  /**
378
- * Exact string fed to HMAC-SHA256: `<timestamp>.<raw body>`. The timestamp is covered by the MAC
379
- * so that a captured request cannot be replayed with a fresh timestamp.
407
+ * Exact string fed to HMAC-SHA256, one field per line, in this order:
408
+ *
409
+ * forge-signer-v2\n<METHOD>\n<path?query>\n<timestamp>\n<nonce>\n<body sha256>
410
+ *
411
+ * Every field has a strict format that excludes the line feed, and the field count is fixed, so
412
+ * the string decodes to exactly one tuple: a signature made for one method, route, query, time,
413
+ * nonce or body is worth nothing for any other. Throws on a field outside its format, so neither
414
+ * side can sign or check an ambiguous string.
380
415
  */
381
- export declare function signerSignedPayload(timestamp: string, rawBody: string): string;
416
+ export declare function signerSignedPayload(fields: SignerSignedFields): string;
382
417
  export declare const SIGNER_ROUTES: {
383
418
  readonly configs: {
384
419
  readonly method: "POST";
package/dist/api.js CHANGED
@@ -51,6 +51,7 @@ export const WEB_API_ROUTES = {
51
51
  launchpadGet: { method: 'GET', path: '/api/launchpads/:id' },
52
52
  claimTransaction: { method: 'GET', path: '/api/launchpads/:id/claim-transaction' },
53
53
  launchpadReactivate: { method: 'POST', path: '/api/launchpads/:id/reactivate' },
54
+ launchpadRetry: { method: 'POST', path: '/api/launchpads/:id/retry' },
54
55
  rpc: { method: 'POST', path: '/api/rpc' },
55
56
  };
56
57
  // POST /api/auth/nonce
@@ -90,6 +91,8 @@ export const GatingResponse = z.object({
90
91
  /** Wallet raw balance (smallest units). */
91
92
  balance: RawTokenAmount,
92
93
  enabled: z.boolean(),
94
+ /** Decimals of the $FORGE mint, read on-chain with the balance; absent when nothing was read. */
95
+ decimals: z.number().int().min(0).max(18).optional(),
93
96
  });
94
97
  // GET /api/flags — public kill-switch state needed by the UI
95
98
  export const FlagsResponse = z.object({ signupsPaused: z.boolean() });
@@ -112,6 +115,12 @@ export const LaunchpadModificationRequest = z.strictObject({
112
115
  request: z.string().min(1),
113
116
  });
114
117
  export const LaunchpadModificationResponse = z.object({ jobId: Uuid });
118
+ /*
119
+ * POST /api/launchpads/:id/retry (no body) — "start again": a draft whose creation was refunded
120
+ * gets a new `create_launchpad` job in `spec_ready`, built from the launchpad's stored spec (same
121
+ * name and subdomain). Idempotent: while that job awaits payment, the same job id is returned.
122
+ */
123
+ export const LaunchpadRetryResponse = z.object({ jobId: Uuid });
115
124
  // POST /api/payments/quote
116
125
  export const PaymentsQuoteRequest = z.strictObject({ jobId: Uuid, kind: PaymentKind });
117
126
  /** Quote as seen by the UI (docs/frontend/CONTRACT.md). */
@@ -219,18 +228,52 @@ export const RpcResponse = z.object({
219
228
  // ---------------------------------------------------------------------------
220
229
  // Internal builder -> signer API (§7)
221
230
  // ---------------------------------------------------------------------------
222
- /** Hex HMAC-SHA256 computed with SIGNER_HMAC_SECRET over `signerSignedPayload(...)`. */
231
+ /** Lowercase hex HMAC-SHA256 computed with SIGNER_HMAC_SECRET over `signerSignedPayload(...)`. */
223
232
  export const SIGNER_SIGNATURE_HEADER = 'X-Forge-Signature';
224
233
  /** Unix time in seconds, as a decimal string. */
225
234
  export const SIGNER_TIMESTAMP_HEADER = 'X-Forge-Timestamp';
235
+ /**
236
+ * 16 random bytes as 32 lowercase hex characters, fresh for every request (and every retry). The
237
+ * signer accepts a nonce once within the clock-skew window.
238
+ */
239
+ export const SIGNER_NONCE_HEADER = 'X-Forge-Nonce';
226
240
  /** Requests whose timestamp differs from the signer clock by more than this are rejected. */
227
241
  export const SIGNER_MAX_CLOCK_SKEW_SECONDS = 60;
242
+ /** First line of the signed string. Any change to its layout gets a new tag. */
243
+ export const SIGNER_SIGNATURE_VERSION = 'forge-signer-v2';
244
+ const SIGNED_FIELD_FORMATS = {
245
+ method: /^[A-Z]{1,16}$/,
246
+ // Origin-form target: printable ASCII, no space, no control character.
247
+ path: /^\/[\x21-\x7e]{0,2047}$/,
248
+ timestamp: /^[0-9]{1,12}$/,
249
+ nonce: /^[0-9a-f]{32}$/,
250
+ bodySha256: /^[0-9a-f]{64}$/,
251
+ };
228
252
  /**
229
- * Exact string fed to HMAC-SHA256: `<timestamp>.<raw body>`. The timestamp is covered by the MAC
230
- * so that a captured request cannot be replayed with a fresh timestamp.
253
+ * Exact string fed to HMAC-SHA256, one field per line, in this order:
254
+ *
255
+ * forge-signer-v2\n<METHOD>\n<path?query>\n<timestamp>\n<nonce>\n<body sha256>
256
+ *
257
+ * Every field has a strict format that excludes the line feed, and the field count is fixed, so
258
+ * the string decodes to exactly one tuple: a signature made for one method, route, query, time,
259
+ * nonce or body is worth nothing for any other. Throws on a field outside its format, so neither
260
+ * side can sign or check an ambiguous string.
231
261
  */
232
- export function signerSignedPayload(timestamp, rawBody) {
233
- return `${timestamp}.${rawBody}`;
262
+ export function signerSignedPayload(fields) {
263
+ for (const key of Object.keys(SIGNED_FIELD_FORMATS)) {
264
+ const value = fields[key];
265
+ if (typeof value !== 'string' || !SIGNED_FIELD_FORMATS[key].test(value)) {
266
+ throw new TypeError(`signer signed payload: invalid ${key}`);
267
+ }
268
+ }
269
+ return [
270
+ SIGNER_SIGNATURE_VERSION,
271
+ fields.method,
272
+ fields.path,
273
+ fields.timestamp,
274
+ fields.nonce,
275
+ fields.bodySha256,
276
+ ].join('\n');
234
277
  }
235
278
  export const SIGNER_ROUTES = {
236
279
  configs: { method: 'POST', path: '/v1/configs' },
@@ -12,8 +12,11 @@ export declare const PRICING: {
12
12
  export declare const FEES: {
13
13
  /** Our fee on our own buy button (before graduation). */
14
14
  readonly platformFeeBps: 30;
15
- /** After graduation; Jupiter keeps 20 % of it. */
16
- readonly jupiterIntegratorFeeBps: 30;
15
+ /**
16
+ * Jupiter Plugin referral fee (`referralFee`), after graduation only; Jupiter keeps 20 % of it.
17
+ * Jupiter Ultra accepts 50 to 255 bps (checked by @forge/core `assertJupiterReferralFeeBps`).
18
+ */
19
+ readonly jupiterIntegratorFeeBps: 50;
17
20
  /** FORGE creator share on the coin of every launchpad. */
18
21
  readonly forgeCreatorSharePct: 25;
19
22
  };
package/dist/constants.js CHANGED
@@ -12,8 +12,11 @@ export const PRICING = {
12
12
  export const FEES = {
13
13
  /** Our fee on our own buy button (before graduation). */
14
14
  platformFeeBps: 30,
15
- /** After graduation; Jupiter keeps 20 % of it. */
16
- jupiterIntegratorFeeBps: 30,
15
+ /**
16
+ * Jupiter Plugin referral fee (`referralFee`), after graduation only; Jupiter keeps 20 % of it.
17
+ * Jupiter Ultra accepts 50 to 255 bps (checked by @forge/core `assertJupiterReferralFeeBps`).
18
+ */
19
+ jupiterIntegratorFeeBps: 50,
17
20
  /** FORGE creator share on the coin of every launchpad. */
18
21
  forgeCreatorSharePct: 25,
19
22
  };
package/dist/db.d.ts CHANGED
@@ -13,6 +13,23 @@ export declare const LaunchpadStatus: z.ZodEnum<{
13
13
  disabled: "disabled";
14
14
  }>;
15
15
  export type LaunchpadStatus = z.infer<typeof LaunchpadStatus>;
16
+ /**
17
+ * Shape of `launchpads.site_host` (migration 0011 holds the same CHECK): a lowercase DNS name
18
+ * without scheme, port, path or trailing dot. Under `vercel.app` (Vercel's production aliases),
19
+ * exactly one label; `vercel.app` itself is never a site host.
20
+ */
21
+ export declare function isSiteHost(value: unknown): value is string;
22
+ /** A site host on `vercel.app` (`<one label>.vercel.app`). */
23
+ export declare function isVercelAppHost(value: unknown): value is string;
24
+ /**
25
+ * The recorded `site_host` of the launchpad `slug`, when a reader may trust it (relay origins,
26
+ * upload `auth.host`, links to the site): exactly `<one label>.vercel.app` (an alias of the
27
+ * launchpad's own Vercel project, checked by the builder before it records one), or exactly
28
+ * `<slug>.<clientsDomain>`. Any other host, however well formed (another domain, the subdomain of
29
+ * another launchpad), is ignored: null. `clientsDomain` is lowercase (readClientsDomain).
30
+ */
31
+ export declare function trustedSiteHost(siteHost: unknown, slug: string, clientsDomain: string | null): string | null;
32
+ export declare const SiteHost: z.ZodString & z.ZodType<string, string, z.core.$ZodTypeInternals<string, string>>;
16
33
  export declare const PaymentStatus: z.ZodEnum<{
17
34
  refunded: "refunded";
18
35
  quoted: "quoted";
@@ -39,6 +56,9 @@ export declare const FlagKey: z.ZodEnum<{
39
56
  claims_paused: "claims_paused";
40
57
  cashbox_sweep_paused: "cashbox_sweep_paused";
41
58
  refunds_paused: "refunds_paused";
59
+ jupiter_claims_paused: "jupiter_claims_paused";
60
+ convert_paused: "convert_paused";
61
+ revenue_paused: "revenue_paused";
42
62
  }>;
43
63
  export type FlagKey = z.infer<typeof FlagKey>;
44
64
  /** ISO 8601 timestamp as returned by PostgREST for `timestamptz`. */
@@ -70,6 +90,13 @@ export interface LaunchpadRow {
70
90
  status: LaunchpadStatus;
71
91
  last_trade_at: Timestamptz | null;
72
92
  created_at: Timestamptz | null;
93
+ /**
94
+ * Exact production host of the client site (`SiteHost`), recorded by the builder from the Vercel
95
+ * API when the site goes live: the verified `<slug>.<CLIENTS_DOMAIN>`, else a `*.vercel.app`
96
+ * alias of the launchpad's own Vercel project. Trusted by exact match only (RPC relay origins,
97
+ * coin uploads, site links). Null until recorded (migration 0011).
98
+ */
99
+ site_host: string | null;
73
100
  }
74
101
  /**
75
102
  * `jobs.owner_tx_details` (jsonb): the public accounts of the launch transaction stored in
package/dist/db.js CHANGED
@@ -6,6 +6,42 @@ import { SolanaAddress } from './spec.js';
6
6
  * it has a default.
7
7
  */
8
8
  export const LaunchpadStatus = z.enum(['draft', 'live', 'sleeping', 'disabled']);
9
+ /** One DNS label: lowercase letters, digits and inner dashes, 1 to 63 characters. */
10
+ const HOST_LABEL = '[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?';
11
+ const HOST_NAME_RE = new RegExp(`^(?=.{4,253}$)(?:${HOST_LABEL}\\.)+[a-z]{2,63}$`);
12
+ const VERCEL_APP_HOST_RE = new RegExp(`^${HOST_LABEL}\\.vercel\\.app$`);
13
+ /**
14
+ * Shape of `launchpads.site_host` (migration 0011 holds the same CHECK): a lowercase DNS name
15
+ * without scheme, port, path or trailing dot. Under `vercel.app` (Vercel's production aliases),
16
+ * exactly one label; `vercel.app` itself is never a site host.
17
+ */
18
+ export function isSiteHost(value) {
19
+ if (typeof value !== 'string' || value === 'vercel.app' || !HOST_NAME_RE.test(value)) {
20
+ return false;
21
+ }
22
+ return !value.endsWith('.vercel.app') || VERCEL_APP_HOST_RE.test(value);
23
+ }
24
+ /** A site host on `vercel.app` (`<one label>.vercel.app`). */
25
+ export function isVercelAppHost(value) {
26
+ return isSiteHost(value) && value.endsWith('.vercel.app');
27
+ }
28
+ /**
29
+ * The recorded `site_host` of the launchpad `slug`, when a reader may trust it (relay origins,
30
+ * upload `auth.host`, links to the site): exactly `<one label>.vercel.app` (an alias of the
31
+ * launchpad's own Vercel project, checked by the builder before it records one), or exactly
32
+ * `<slug>.<clientsDomain>`. Any other host, however well formed (another domain, the subdomain of
33
+ * another launchpad), is ignored: null. `clientsDomain` is lowercase (readClientsDomain).
34
+ */
35
+ export function trustedSiteHost(siteHost, slug, clientsDomain) {
36
+ if (!isSiteHost(siteHost))
37
+ return null;
38
+ if (isVercelAppHost(siteHost))
39
+ return siteHost;
40
+ return clientsDomain !== null && siteHost === `${slug}.${clientsDomain}` ? siteHost : null;
41
+ }
42
+ export const SiteHost = z.string().refine(isSiteHost, {
43
+ error: 'Must be a lowercase host name (one label under vercel.app)',
44
+ });
9
45
  export const PaymentStatus = z.enum(['quoted', 'confirmed', 'expired', 'refunded']);
10
46
  export const PaymentKind = z.enum(['creation', 'modification']);
11
47
  export const ChatRole = z.enum(['user', 'assistant', 'system']);
@@ -17,6 +53,9 @@ export const FlagKey = z.enum([
17
53
  'claims_paused',
18
54
  'cashbox_sweep_paused',
19
55
  'refunds_paused',
56
+ 'jupiter_claims_paused',
57
+ 'convert_paused',
58
+ 'revenue_paused',
20
59
  ]);
21
60
  /**
22
61
  * `jobs.owner_tx_details` (jsonb): the public accounts of the launch transaction stored in
@@ -33,6 +33,11 @@ export interface LaunchpadSummary {
33
33
  id: string;
34
34
  name: string;
35
35
  slug: string;
36
+ /**
37
+ * Where the live site is: `https://<site_host>` when the builder recorded its exact host
38
+ * (`launchpads.site_host`), else `https://<slug>.<CLIENTS_DOMAIN>`. Null unless the launchpad is
39
+ * live or sleeping (or when neither is known).
40
+ */
36
41
  siteUrl: string | null;
37
42
  status: LaunchpadStatus;
38
43
  coin: {
@@ -45,7 +50,32 @@ export interface LaunchpadSummary {
45
50
  claimablePartnerFeesLamports: Lamports;
46
51
  includedModificationsLeft: number;
47
52
  activeJobId: string | null;
48
- }
53
+ /** Status of `activeJobId` (null without one). Absent from older servers. */
54
+ activeJobStatus?: JobStatus | null;
55
+ /**
56
+ * A draft whose creation was refunded, with nothing in progress: `retryLaunchpad` can start it
57
+ * again with the same blueprint. The server re-checks everything (payments, refunds, kill
58
+ * switches, access) when asked. Absent from older servers: then treated as false.
59
+ */
60
+ canStartAgain?: boolean;
61
+ /**
62
+ * A draft whose creation jobs were all refunded but that cannot be started again yet, and why
63
+ * (null otherwise, and whenever a job is open: its tracker explains). The same rules as the
64
+ * route, so "start again" is never offered and then refused. Absent from older servers.
65
+ */
66
+ startAgainBlocked?: StartAgainBlockedReason | null;
67
+ }
68
+ /**
69
+ * Why "start again" is not available for a refunded draft:
70
+ * - `refund_pending`: money of an earlier attempt is still owed back (refund not recorded, or a
71
+ * payment anomaly waiting for its manual refund);
72
+ * - `payment_pending`: a payment for an earlier attempt may still land or be confirmed;
73
+ * - `needs_support`: the team has to look at it first (on-chain setup recorded, or the limit of
74
+ * refunded creations reached);
75
+ * - `invalid_spec`: the stored blueprint no longer passes the checks (design it again).
76
+ */
77
+ export declare const START_AGAIN_BLOCKED_REASONS: readonly ["refund_pending", "payment_pending", "needs_support", "invalid_spec"];
78
+ export type StartAgainBlockedReason = (typeof START_AGAIN_BLOCKED_REASONS)[number];
49
79
  export interface LaunchpadDetail extends LaunchpadSummary {
50
80
  /** Complete spec. */
51
81
  spec: LaunchpadSpecDraft;
@@ -167,6 +197,10 @@ export interface WebClient {
167
197
  useLaunchpad(id: string): UseLaunchpadResult;
168
198
  claimPartnerFees(launchpadId: string, onPhase: OnTxPhase): Promise<TxResult>;
169
199
  reactivateLaunchpad(launchpadId: string): Promise<void>;
200
+ /** "Start again" on a refunded draft: the new (or still unpaid) creation job, to pay next. */
201
+ retryLaunchpad(launchpadId: string): Promise<{
202
+ jobId: string;
203
+ }>;
170
204
  formatSol(lamports: Lamports, opts?: FormatSolOptions): string;
171
205
  formatUsd(n: number): string;
172
206
  shortAddress(a: string): string;
@@ -1,3 +1,18 @@
1
+ /**
2
+ * Why "start again" is not available for a refunded draft:
3
+ * - `refund_pending`: money of an earlier attempt is still owed back (refund not recorded, or a
4
+ * payment anomaly waiting for its manual refund);
5
+ * - `payment_pending`: a payment for an earlier attempt may still land or be confirmed;
6
+ * - `needs_support`: the team has to look at it first (on-chain setup recorded, or the limit of
7
+ * refunded creations reached);
8
+ * - `invalid_spec`: the stored blueprint no longer passes the checks (design it again).
9
+ */
10
+ export const START_AGAIN_BLOCKED_REASONS = [
11
+ 'refund_pending',
12
+ 'payment_pending',
13
+ 'needs_support',
14
+ 'invalid_spec',
15
+ ];
1
16
  export const CLIENT_ERROR_CODES = [
2
17
  'RATE_LIMITED',
3
18
  'UNAUTHORIZED',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forgedevv/shared",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Shared contracts (constants, zod schemas, types) between all FORGE apps. Published on npm as @forgedevv/shared.",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {
@@ -29,7 +29,8 @@
29
29
  "files": [
30
30
  "dist/**/*.js",
31
31
  "dist/**/*.d.ts",
32
- "!dist/**/*.test.*"
32
+ "!dist/**/*.test.*",
33
+ "CHANGELOG.md"
33
34
  ],
34
35
  "publishConfig": {
35
36
  "access": "public",