@phpsandbox/sdk 0.6.0-canary.0 → 0.6.0-canary.10

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/README.md CHANGED
@@ -57,7 +57,7 @@ PHPSandbox.rest('token', 'https://api.phpsandbox.io/v1', {
57
57
 
58
58
  ## App server integration
59
59
 
60
- Apps hosted on PHPSandbox can use one environment-scoped identity for Customers and Payments:
60
+ Apps hosted on PHPSandbox can use one environment-scoped identity for Customers, Payments, and feedback:
61
61
 
62
62
  ```ts
63
63
  import { PHPSandbox } from '@phpsandbox/sdk';
@@ -76,9 +76,18 @@ const checkout = await platform.payments.checkout({
76
76
  successPath: '/thanks',
77
77
  cancelPath: '/account',
78
78
  });
79
+ await platform.feedback.submit({
80
+ type: 'question',
81
+ message: feedback.message,
82
+ author: { externalId: account.customerExternalId },
83
+ }, { idempotencyKey: feedback.id });
79
84
  ```
80
85
 
81
- Persist an opaque UUID on each app account for its external customer identity; separate databases must not reuse sequential IDs. Initialize this client only on the app server. It reads the provisioned app configuration and exposes only app runtime capabilities, without notebook or organization administration. Production uses its own identity and never falls back to development. Select `{ mode: 'test' }` once for an intentional sandbox run. Runtimes without `process.env` can supply server bindings through `{ env }`.
86
+ Persist an opaque UUID on each app account for its external customer identity; separate databases must not reuse sequential IDs. Initialize this client only on the app server. It reads the provisioned app configuration and exposes only connected app runtime capabilities, without notebook or organization administration. Production uses its own identity and never falls back to development. Use `{ mode: 'test' }` only in development. Runtimes without `process.env` can supply server bindings through `{ env }`.
87
+
88
+ Connect feedback before publishing using your builder client: `await client.feedback.forProject('PROJECT_ID').provision('live')`. Use `test` for development previews. The app credential can submit feedback but cannot read the seller inbox or change feedback status. Save messages in your app first and forward with a stable idempotency key so a temporary delivery failure does not lose the buyer's message or create duplicates.
89
+
90
+ The SDK is optional: any backend, including PHP, can send the same JSON payload to `POST /v1/platform/feedback/runtime` with the environment's app credential in `Authorization: Bearer APP_TOKEN` and a stable `Idempotency-Key` header. Keep that credential on the server; buyers sign in to your app and do not need a PHPSandbox account.
82
91
 
83
92
  Applications can configure their own organization conversion stages and one event follow-up. Configuration never activates a journey:
84
93
 
@@ -172,7 +181,6 @@ Each `NotebookInstance` exposes service clients:
172
181
  - `notebook.auth` (`Auth`)
173
182
  - `notebook.services` (`Services`)
174
183
  - `notebook.mail` (`NotebookMail`)
175
- - `notebook.feedback` (`Feedback`)
176
184
 
177
185
  Notes:
178
186
 
@@ -209,6 +217,32 @@ const [hasMore, matches] = await notebook.files.search(
209
217
  );
210
218
  ```
211
219
 
220
+ `files.download()` returns a `ReadableStream<Uint8Array>` containing the notebook ZIP.
221
+ Read it incrementally or pipe it to a writable stream:
222
+
223
+ ```ts
224
+ const controller = new AbortController();
225
+ const archive = notebook.files.download({
226
+ include: ['src/**'],
227
+ exclude: ['vendor/**'],
228
+ signal: controller.signal,
229
+ });
230
+
231
+ for await (const chunk of archive) {
232
+ await consume(chunk);
233
+ }
234
+ ```
235
+
236
+ Downloads use HTTP for both realtime and REST clients and require the runtime's
237
+ `POST /api/v1/notebooks/{notebook}/runtime/files/download` endpoint. Reading starts
238
+ the request; cancelling the stream, aborting its signal, or disposing the notebook
239
+ stops the transfer. Failures reject stream reads and are not retried after bytes
240
+ have started arriving.
241
+
242
+ **Breaking change in 0.6.0-canary.4:** downloads no longer return a `Blob` or accept
243
+ `onChunk`. If you need a Blob, explicitly collect the archive with
244
+ `await new Response(notebook.files.download()).blob()`; this buffers the ZIP in memory.
245
+
212
246
  ### Terminal
213
247
 
214
248
  ```ts
@@ -375,24 +409,39 @@ await notebook.mail.delete(mail.hash);
375
409
  await notebook.mail.disable();
376
410
  ```
377
411
 
412
+ ### Product features
413
+
414
+ Projects own the product lifecycle. Address Customers, Payments, feedback, and publishing with an existing project ID; creating a standalone compute sandbox does not create a project.
415
+
416
+ This replaces `notebook.feedback`, `notebook.payments`, `notebook.publication`, and the `forNotebook` helpers. Use a project ID explicitly; sandbox IDs are not aliases for projects.
417
+
418
+ ```ts
419
+ const customers = client.customers.forProject('PROJECT_ID');
420
+ const payments = client.payments.forProject('PROJECT_ID');
421
+ const publishing = client.publications.forProject('PROJECT_ID');
422
+ ```
423
+
378
424
  ### Feedback
379
425
 
380
426
  ```ts
381
- await notebook.feedback.configure({
427
+ // Feedback uses the same API token as the rest of the SDK.
428
+ // Product features require an existing project ID, not a sandbox ID.
429
+ const projectFeedback = client.feedback.forProject('PROJECT_ID');
430
+ await projectFeedback.configure({
382
431
  widget: {
383
432
  targets: ['preview', 'publication'],
384
433
  types: ['bug', 'suggestion', 'question'],
385
434
  },
386
435
  });
387
436
 
388
- const feedback = await notebook.feedback.submit({
437
+ const feedback = await projectFeedback.submit({
389
438
  type: 'bug',
390
439
  message: 'Saving the profile failed.',
391
440
  author: { externalId: 'customer-42', email: 'ada@example.com' },
392
441
  context: { url: 'https://app.example.com/profile', path: '/profile' },
393
442
  });
394
443
 
395
- await notebook.feedback.update(feedback.id, { status: 'resolved' });
444
+ await projectFeedback.update(feedback.id, { status: 'resolved' });
396
445
  ```
397
446
 
398
447
  The server-side SDK uses the same PHPSandbox API token as the rest of the sandbox. Set `widget: null` to remove the optional popout without disabling API ingestion.
@@ -450,42 +499,38 @@ MIT
450
499
 
451
500
  ### Publishing apps
452
501
 
453
- Save your app configuration, prepare a proposal for the apps you want to publish, and submit that proposal after explicit confirmation. Use `notebook.secrets` for production secrets and `notebook.publication.catalog` to discover destinations and available resources.
502
+ Save your app configuration, prepare a proposal for the apps you want to publish, and submit that proposal after explicit confirmation. Use `notebook.secrets` for production secrets and `client.publications.forProject(projectId).catalog` to discover destinations and available resources.
454
503
 
455
504
  ```ts
456
- const notebook = await client.notebook.get('SANDBOX_ID');
457
- try {
458
- const configuration = await notebook.publication.updateConfiguration({
459
- expectedRevision: 0,
460
- operations: [{ op: 'add-app', app: {
461
- name: 'Backend', kind: 'http', root: '.',
462
- build: { mode: 'dockerfile', context: '.', dockerfile: { source: 'project', path: 'Dockerfile' } },
463
- runtime: { mode: 'container', port: 3333, healthCheck: { path: '/' } },
464
- destination: { provider: 'ssh-server', serverId: 'OWNED_SERVER_ID' },
465
- routing: { mode: 'managed' }, resourceIds: [],
466
- } }],
467
- });
468
- const appIds = [configuration.apps[0].id];
469
- const plan = await notebook.publication.prepare({
470
- expectedRevision: configuration.revision, appIds,
505
+ const publisher = client.publications.forProject('PROJECT_ID');
506
+ const configuration = await publisher.updateConfiguration({
507
+ expectedRevision: 0,
508
+ operations: [{ op: 'add-app', app: {
509
+ name: 'Backend', kind: 'http', root: '.',
510
+ build: { mode: 'dockerfile', context: '.', dockerfile: { source: 'project', path: 'Dockerfile' } },
511
+ runtime: { mode: 'container', port: 3333, healthCheck: { path: '/' } },
512
+ destination: { provider: 'ssh-server', serverId: 'OWNED_SERVER_ID' },
513
+ routing: { mode: 'managed' }, resourceIds: [],
514
+ } }],
515
+ });
516
+ const appIds = [configuration.apps[0].id];
517
+ const plan = await publisher.prepare({
518
+ expectedRevision: configuration.revision, appIds,
519
+ });
520
+ if (!plan.review.ready) {
521
+ console.log(plan.review.blockers);
522
+ } else {
523
+ // Show this proposal to the user and wait for their confirmation.
524
+ const result = await publisher.publish({
525
+ reviewId: plan.review.id, proposal: plan.review.proposal, appIds,
471
526
  });
472
- if (!plan.review.ready) {
473
- console.log(plan.review.blockers);
527
+ if ('confirmationRequired' in result) {
528
+ // Show the changes and updated proposal; obtain a new confirmation.
529
+ console.log(result.changes, result.plan);
474
530
  } else {
475
- // Show this proposal to the user and wait for their confirmation.
476
- const result = await notebook.publication.publish({
477
- reviewId: plan.review.id, proposal: plan.review.proposal, appIds,
478
- });
479
- if ('confirmationRequired' in result) {
480
- // Show the changes and updated proposal; obtain a new confirmation.
481
- console.log(result.changes, result.plan);
482
- } else {
483
- const current = await notebook.publication.application(result.id);
484
- console.log(current.status); // publishing, healthy, partial, failed, or deleted
485
- }
531
+ const current = await publisher.application(result.id);
532
+ console.log(current.status); // publishing, healthy, partial, failed, or deleted
486
533
  }
487
- } finally {
488
- notebook.dispose();
489
534
  }
490
535
  ```
491
536
 
@@ -497,6 +542,21 @@ Inspect deployment, runtime and routing outcomes for each selected app; a partia
497
542
 
498
543
  Shared managed-resource creation currently starts with one app. After creation, update the resource definition to `reuse` with the catalog resource ID before attaching other apps. Simultaneous first-time shared-resource creation is unsupported.
499
544
 
545
+ #### Custom domains
546
+
547
+ For a healthy Laravel Cloud publication, choose the DNS setup when creating the domain:
548
+
549
+ ```ts
550
+ const publication = await client.publications.get('PUBLICATION_ID');
551
+ const domain = await publication.domains.create('app.example.com', {
552
+ cloudflareStrategy: 'dns_proxy',
553
+ allowDowntime: false,
554
+ });
555
+ console.log(domain.dnsInstructions);
556
+ ```
557
+
558
+ Use `none` for another DNS provider, `dns` for Cloudflare DNS only, or `dns_proxy` for Cloudflare's orange-cloud proxy. `allowDowntime: false` prepares verification before traffic moves; `true` allows routing first. Omitted options preserve the existing `none` / uninterrupted defaults. These optional settings require an API version that supports domain DNS and transfer choices. Existing provider domains are inspected rather than reconfigured.
559
+
500
560
  #### Browser publishing streams through your backend
501
561
 
502
562
  Keep service-account credentials on your server. After authorizing project access and checking the requested publishing attempt, open the original SSE bytes with `publication.stream()`. Return the SDK's standard `Response` directly from your framework:
@@ -534,3 +594,23 @@ for await (const event of readPublicationStream(response)) {
534
594
  ```
535
595
 
536
596
  `readPublicationStream` also accepts a `ReadableStream<Uint8Array>`. By default, it rejects EOF without a final result (including empty streams). Use `{ requireResult: false }` only when deliberately consuming a stream fragment. It validates recognized event payloads, throws for server error events, and ignores unknown event names and heartbeat comments. Ending iteration early cancels the stream; aborting the fetch interrupts a pending read. Disconnecting observation does not cancel the server-side deployment. `PublicationRun.events()` uses this same parser.
597
+
598
+ ### App access
599
+
600
+ Configure preview access and inherited access for apps published through the managed edge:
601
+
602
+ ```ts
603
+ await notebook.preview.configure({ mode: 'password', password: 'a-long-private-password' });
604
+ await notebook.preview.configure({
605
+ mode: 'email',
606
+ allowedEmails: ['person@example.com', '*@company.test'],
607
+ });
608
+ await notebook.preview.configure({ mode: 'external', authorizationUrl: 'https://auth.example.com/access' });
609
+ await notebook.preview.configure({ mode: 'public' });
610
+ ```
611
+
612
+ Email mode presents email and code forms on the app's domain. Supply between 1 and 100 exact email addresses or domain rules such as `*@company.test`. Domain rules match that exact domain, excluding subdomains. The platform sends and verifies the codes; no custom verification service or callback configuration is required. Protected mode changes rotate the access version, invalidating previous grants and pending codes. `preview.get()` includes the current mode and email rules.
613
+
614
+ External mode redirects visitors to the configured HTTPS URL with the app URL in `returnTo`. Your service decides whether to grant access, obtains an access URL through the authenticated API, and returns the visitor using that opaque URL. Keep API credentials on your server and validate return targets. `preview.get(url)` returns both a clean `url` and a credentialed `accessUrl`; open `accessUrl` without reconstructing it.
615
+
616
+ **Breaking change:** Use `configure({ mode: 'password', password })` instead of `setPassword()` and `configure({ mode: 'public' })` instead of `disable()`; the two older methods and `SetNotebookPreviewInput` have been removed. Access responses now require `mode`, with email rules or an authorization URL required for the corresponding mode. Publication password updates also require `password`.
package/dist/app.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { Client } from './index.js';
2
2
  import { CustomerRuntime } from './customers.js';
3
3
  import { PaymentRuntime, type PaymentMode } from './payments.js';
4
+ import { FeedbackRuntime } from './feedback.js';
4
5
  export interface AppRuntimeOptions {
5
6
  /** Defaults to live in production and test otherwise. There is no fallback between modes. */
6
7
  mode?: PaymentMode;
@@ -17,5 +18,6 @@ export declare class AppRuntime {
17
18
  readonly mode: PaymentMode;
18
19
  readonly customers: CustomerRuntime;
19
20
  readonly payments: PaymentRuntime;
21
+ readonly feedback: FeedbackRuntime;
20
22
  constructor(client: Client, token: string, mode: PaymentMode);
21
23
  }
package/dist/app.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { CustomerRuntime } from './customers.js';
2
2
  import { PaymentRuntime } from './payments.js';
3
+ import { FeedbackRuntime } from './feedback.js';
3
4
  export function resolveAppConfiguration(options) {
4
5
  if (typeof window !== 'undefined' && typeof document !== 'undefined') {
5
6
  throw new Error('Application credentials must only be used on a trusted server.');
@@ -9,7 +10,7 @@ export function resolveAppConfiguration(options) {
9
10
  const token = environment[`PHPSANDBOX_APP_${mode.toUpperCase()}_TOKEN`];
10
11
  const url = environment.PHPSANDBOX_APP_API_URL;
11
12
  if (!token || !/^happ_[a-f0-9]{64}$/.test(token) || !url) {
12
- throw new Error(`Connect your app's ${mode === 'live' ? 'production' : 'development'} platform environment before using Customers or Payments.`);
13
+ throw new Error(`Connect your app's ${mode === 'live' ? 'production' : 'development'} platform environment before using platform services.`);
13
14
  }
14
15
  return { mode, token, url };
15
16
  }
@@ -19,5 +20,6 @@ export class AppRuntime {
19
20
  this.mode = mode;
20
21
  this.customers = new CustomerRuntime(client);
21
22
  this.payments = new PaymentRuntime(client, { token, mode });
23
+ this.feedback = new FeedbackRuntime(client);
22
24
  }
23
25
  }