@segmentstream-cloud/app 0.14.0 → 0.16.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/README.md +40 -6
- package/dist/app.d.ts +3 -0
- package/dist/app.js +3 -1
- package/dist/data.d.ts +2 -1
- package/dist/data.js +11 -6
- package/dist/decision-pages.js +8 -2
- package/dist/index.d.ts +1 -0
- package/dist/jobs.d.ts +49 -0
- package/dist/jobs.js +40 -0
- package/dist/logo.d.ts +1 -0
- package/dist/logo.js +25 -0
- package/dist/oauth-completion.d.ts +6 -0
- package/dist/oauth-completion.js +57 -0
- package/dist/oauth.d.ts +1 -0
- package/dist/oauth.js +34 -2
- package/dist/ui/index.d.ts +1 -0
- package/dist/ui/index.js +1 -0
- package/dist/ui/segmentstream-logo.d.ts +2 -0
- package/dist/ui/segmentstream-logo.js +7 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -412,8 +412,8 @@ One authorization model covers the ontology: every `segmentstream.ontology` call
|
|
|
412
412
|
included, runs as the signed-in user through the ontology service, which checks their
|
|
413
413
|
permissions. `segmentstream.ontology.<entity>.aggregate(request)` posts to
|
|
414
414
|
`/api/ontology/<entity>/aggregate` and needs read permission on the entities it reaches.
|
|
415
|
-
`segmentstream.warehouse`
|
|
416
|
-
|
|
415
|
+
`segmentstream.warehouse` uses the workspace app credential and reaches the whole workspace
|
|
416
|
+
warehouse. The job helpers below use it for explicitly opted-in jobs. `segmentstream.fetch(path)` still requires the legacy
|
|
417
417
|
platform session. Aggregates through the service need the workspace's ontology deployed with
|
|
418
418
|
`@segmentstream-cloud/ontology` 0.13.0 or later.
|
|
419
419
|
|
|
@@ -529,9 +529,7 @@ const { fields, rows } = await segmentstream.ontology.aggregate({
|
|
|
529
529
|
dimensions: ["acmeSales.orderDate"],
|
|
530
530
|
granularity: "month",
|
|
531
531
|
granularityOf: "acmeSales.orderDate",
|
|
532
|
-
filters: [
|
|
533
|
-
{ dimension: "acmeSales.orderDate", operator: "inPeriod", value: "this year to date" },
|
|
534
|
-
],
|
|
532
|
+
filters: [{ dimension: "acmeSales.orderDate", operator: "inPeriod", value: "this year to date" }],
|
|
535
533
|
orderBy: [{ measure: "planAttainment", direction: "desc" }],
|
|
536
534
|
});
|
|
537
535
|
// fields[1]: { name: "planAttainment", label: "Plan attainment", unit: "ratio", additive: false,
|
|
@@ -553,6 +551,42 @@ const { fields, rows } = await segmentstream.ontology.aggregate({
|
|
|
553
551
|
0.23.0 or later, which declares and serves measures of the model; earlier versions refuse
|
|
554
552
|
them. Requests that name none work as before, and their fields carry no `inputs`.
|
|
555
553
|
|
|
554
|
+
## Start a pipeline job
|
|
555
|
+
|
|
556
|
+
With app SDK 0.15.0 and pipeline SDK 0.1.0a65 or later, opt in a job and deploy it:
|
|
557
|
+
|
|
558
|
+
```python
|
|
559
|
+
s.job("land_watch", assets=[land_checks], allow_app_runs=True)
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
In server code, check the user's role or permissions before calling on their behalf:
|
|
563
|
+
|
|
564
|
+
```ts
|
|
565
|
+
const user = await segmentstream.requireUser();
|
|
566
|
+
if (!user.roles.includes("operator")) {
|
|
567
|
+
throw new Error("An operator role is required.");
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
const run = await segmentstream.jobs.run("land_watch");
|
|
571
|
+
// From a later authorized request, for example a Route Handler the page polls:
|
|
572
|
+
const current = await segmentstream.jobs.getRun(run.id);
|
|
573
|
+
const recent = await segmentstream.jobs.listRuns("land_watch");
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
The helpers use the workspace app credential, read on every call, or fall back to the
|
|
577
|
+
legacy member session. Credential access defaults to denied unless the current deployed
|
|
578
|
+
job sets `allow_app_runs=True`. It stays within the workspace and permits submission and
|
|
579
|
+
status/history reads; run logs, activity and cancellation require platform membership.
|
|
580
|
+
Removing the flag revokes app access to that job's runs.
|
|
581
|
+
|
|
582
|
+
`run()` returns immediately with a run ID and status. It generates a UUID idempotency key
|
|
583
|
+
unless passed `{ idempotencyKey }`; reuse an explicit key when retrying one button action.
|
|
584
|
+
Separate calls with new keys can run concurrently. Partitioned jobs accept
|
|
585
|
+
`{ partitionRange: { startDate, endDate } }`. All helpers accept an optional `signal`.
|
|
586
|
+
Poll `getRun()` until status is `succeeded`, `failed` or `cancelled`, and tolerate future
|
|
587
|
+
status values. Protect both submission and status routes with your app's authorization.
|
|
588
|
+
See [app data access](../../docs/app-data.md#start-a-pipeline-job-from-the-app) for a Route Handler.
|
|
589
|
+
|
|
556
590
|
## How requests are handled
|
|
557
591
|
|
|
558
592
|
- Page navigations without a session are redirected to `/sign-in?returnTo=<path>` and
|
|
@@ -568,7 +602,7 @@ const { fields, rows } = await segmentstream.ontology.aggregate({
|
|
|
568
602
|
| -------------------------------------- | ------------------------------------------------------------------------------- |
|
|
569
603
|
| `NEON_AUTH_BASE_URL` | The workspace's Neon Auth URL. Set: sign in with Neon Auth. Unset: 0.2 sign-in. |
|
|
570
604
|
| `SEGMENTSTREAM_APP_AUTH_COOKIE_SECRET` | 32 or more characters; signs the session cache cookie. Required with Neon Auth. |
|
|
571
|
-
| `SEGMENTSTREAM_APP_CREDENTIAL` | The workspace app credential for warehouse queries.
|
|
605
|
+
| `SEGMENTSTREAM_APP_CREDENTIAL` | The workspace app credential for warehouse queries and opted-in pipeline jobs. |
|
|
572
606
|
| `SEGMENTSTREAM_ONTOLOGY_URL` | Optional: the ontology service when it does not run beside the app. |
|
|
573
607
|
| `SEGMENTSTREAM_URL` | SegmentStream origin. Defaults to `https://cloud.segmentstream.com`. |
|
|
574
608
|
|
package/dist/app.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Environment, SegmentStreamAppConfig } from "./config.js";
|
|
2
2
|
import type { AgentIdentities } from "./agent.js";
|
|
3
3
|
import type { WarehouseClient } from "./data.js";
|
|
4
|
+
import type { JobsClient } from "./jobs.js";
|
|
4
5
|
import type { SegmentStreamRouteHandlers } from "./handlers.js";
|
|
5
6
|
import type { AgentAuthorizations } from "./oauth.js";
|
|
6
7
|
import type { AuthRouteHandlers, CreateNeonAuth, SignOutOptions } from "./neon.js";
|
|
@@ -46,6 +47,8 @@ export interface SegmentStreamApp<O = unknown> {
|
|
|
46
47
|
* (`SEGMENTSTREAM_APP_CREDENTIAL`) when it is set, else the signed-in member's session.
|
|
47
48
|
*/
|
|
48
49
|
readonly warehouse: WarehouseClient;
|
|
50
|
+
/** Start and read pipeline runs with the workspace credential. Server-only; gate app users. */
|
|
51
|
+
readonly jobs: JobsClient;
|
|
49
52
|
/** The app's ontology service, `/api/ontology`, as the signed-in user. Server-only. */
|
|
50
53
|
readonly ontology: OntologyClient<O>;
|
|
51
54
|
/**
|
package/dist/app.js
CHANGED
|
@@ -57,6 +57,7 @@ export function createApp(config, dependencies = {}) {
|
|
|
57
57
|
getAccessToken: legacy.getAccessToken,
|
|
58
58
|
fetch: data.fetch,
|
|
59
59
|
warehouse: data.warehouse,
|
|
60
|
+
jobs: data.jobs,
|
|
60
61
|
ontology: createOntologyClient(ontologyTransport),
|
|
61
62
|
oauth: createAgentAuthorizations(ontologyTransport),
|
|
62
63
|
agent: createAgentIdentities(ontologyTransport),
|
|
@@ -87,6 +88,7 @@ export function createApp(config, dependencies = {}) {
|
|
|
87
88
|
getAccessToken: async () => null,
|
|
88
89
|
fetch: data.fetch,
|
|
89
90
|
warehouse: data.warehouse,
|
|
91
|
+
jobs: data.jobs,
|
|
90
92
|
ontology: createOntologyClient(neon.ontologyTransport),
|
|
91
93
|
oauth: createAgentAuthorizations(neon.ontologyTransport),
|
|
92
94
|
agent: createAgentIdentities(neon.ontologyTransport),
|
|
@@ -97,7 +99,7 @@ async function noSegmentStreamSession() {
|
|
|
97
99
|
throw new SegmentStreamApiError({
|
|
98
100
|
code: "app_credential_missing",
|
|
99
101
|
status: 401,
|
|
100
|
-
detail: "Apps that sign in with Neon Auth have no SegmentStream session. Warehouse queries
|
|
102
|
+
detail: "Apps that sign in with Neon Auth have no SegmentStream session. Warehouse queries and opted-in job runs need SEGMENTSTREAM_APP_CREDENTIAL, which SegmentStream sets on deploy. Generic fetch requires a legacy SegmentStream session.",
|
|
101
103
|
});
|
|
102
104
|
}
|
|
103
105
|
function notConfiguredHandlers() {
|
package/dist/data.d.ts
CHANGED
|
@@ -214,7 +214,7 @@ export interface DataClientDependencies {
|
|
|
214
214
|
readonly getAccessToken: () => Promise<string | null>;
|
|
215
215
|
/**
|
|
216
216
|
* The workspace app credential, `SEGMENTSTREAM_APP_CREDENTIAL`, read on every call. The API
|
|
217
|
-
* accepts it for warehouse queries and
|
|
217
|
+
* accepts it for warehouse queries and opted-in job runs; otherwise they use the session token.
|
|
218
218
|
*/
|
|
219
219
|
readonly getAppCredential?: () => string | undefined;
|
|
220
220
|
readonly sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
|
|
@@ -224,4 +224,5 @@ export interface DataClientDependencies {
|
|
|
224
224
|
export declare function createDataClient(dependencies: DataClientDependencies): {
|
|
225
225
|
fetch: (path: string, init?: RequestInit) => Promise<Response>;
|
|
226
226
|
warehouse: WarehouseClient;
|
|
227
|
+
jobs: import("./jobs.js").JobsClient;
|
|
227
228
|
};
|
package/dist/data.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { SegmentStreamApiError, readJson } from "./errors.js";
|
|
2
2
|
import { encodeParameters } from "./parameters.js";
|
|
3
|
+
import { createJobsClient } from "./jobs.js";
|
|
3
4
|
export { SegmentStreamApiError } from "./errors.js";
|
|
4
5
|
const REQUEST_TIMEOUT_MS = 45_000;
|
|
5
6
|
const DEFAULT_MAX_ROWS = 10_000;
|
|
@@ -17,8 +18,8 @@ export function createDataClient(dependencies) {
|
|
|
17
18
|
function apiFetch(path, init = {}) {
|
|
18
19
|
return authorizedFetch(path, init, dependencies.getAccessToken);
|
|
19
20
|
}
|
|
20
|
-
/**
|
|
21
|
-
async function
|
|
21
|
+
/** Workspace operations prefer the app credential, which needs no SegmentStream session. */
|
|
22
|
+
async function workspaceToken() {
|
|
22
23
|
return dependencies.getAppCredential?.() ?? dependencies.getAccessToken();
|
|
23
24
|
}
|
|
24
25
|
async function authorizedFetch(path, init, getToken) {
|
|
@@ -122,7 +123,7 @@ export function createDataClient(dependencies) {
|
|
|
122
123
|
parameters: encodeParameters(options.params),
|
|
123
124
|
...bytesLimit(options.maximumBytesBilled),
|
|
124
125
|
pageSize: bounds.pageSize,
|
|
125
|
-
}, options.signal,
|
|
126
|
+
}, options.signal, workspaceToken);
|
|
126
127
|
return collect(page, bounds, options.signal);
|
|
127
128
|
}
|
|
128
129
|
async function result(queryId, options = {}) {
|
|
@@ -135,7 +136,7 @@ export function createDataClient(dependencies) {
|
|
|
135
136
|
if (pageToken !== undefined) {
|
|
136
137
|
search.set("pageToken", pageToken);
|
|
137
138
|
}
|
|
138
|
-
return call("GET", `${workspace}/warehouse/queries/${encodeURIComponent(queryId)}?${search.toString()}`, undefined, signal,
|
|
139
|
+
return call("GET", `${workspace}/warehouse/queries/${encodeURIComponent(queryId)}?${search.toString()}`, undefined, signal, workspaceToken);
|
|
139
140
|
}
|
|
140
141
|
const warehouse = {
|
|
141
142
|
query,
|
|
@@ -146,7 +147,7 @@ export function createDataClient(dependencies) {
|
|
|
146
147
|
parameters: encodeParameters(options.params),
|
|
147
148
|
...bytesLimit(options.maximumBytesBilled),
|
|
148
149
|
dryRun: true,
|
|
149
|
-
}, options.signal,
|
|
150
|
+
}, options.signal, workspaceToken);
|
|
150
151
|
return {
|
|
151
152
|
columns: page.columns,
|
|
152
153
|
bytesProcessed: page.bytesProcessed ?? null,
|
|
@@ -154,7 +155,11 @@ export function createDataClient(dependencies) {
|
|
|
154
155
|
};
|
|
155
156
|
},
|
|
156
157
|
};
|
|
157
|
-
|
|
158
|
+
const jobs = createJobsClient({
|
|
159
|
+
workspace,
|
|
160
|
+
fetch: (path, init) => authorizedFetch(path, init, workspaceToken),
|
|
161
|
+
});
|
|
162
|
+
return { fetch: apiFetch, warehouse, jobs };
|
|
158
163
|
}
|
|
159
164
|
function bytesLimit(value) {
|
|
160
165
|
if (value === undefined) {
|
package/dist/decision-pages.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { AGENT_CONNECT_PAGE, CONSENT_PAGE } from "./pages.js";
|
|
2
2
|
import { matchesPathPrefix } from "./proxy.js";
|
|
3
3
|
/** Pages where a signed-in member lets an agent act for them. */
|
|
4
|
-
const DECISION_PAGES = [CONSENT_PAGE, AGENT_CONNECT_PAGE];
|
|
4
|
+
const DECISION_PAGES = [CONSENT_PAGE, AGENT_CONNECT_PAGE, "/sign-in", "/api/oauth/consent"];
|
|
5
5
|
/**
|
|
6
6
|
* The proxy, with the decision pages unframeable: no other site may show them in a frame and trick
|
|
7
7
|
* a member into clicking Allow or Approve. Every app gets this from the SDK, whatever its Next.js
|
|
@@ -12,8 +12,14 @@ export function withUnframeablePages(proxy) {
|
|
|
12
12
|
const response = await proxy(request);
|
|
13
13
|
const pathname = request.nextUrl.pathname;
|
|
14
14
|
if (DECISION_PAGES.some((page) => matchesPathPrefix(pathname, page))) {
|
|
15
|
+
response.headers.set("cache-control", "no-store");
|
|
16
|
+
response.headers.set("referrer-policy", "no-referrer");
|
|
15
17
|
response.headers.set("x-frame-options", "DENY");
|
|
16
|
-
response.headers.
|
|
18
|
+
const policy = response.headers.get("content-security-policy");
|
|
19
|
+
response.headers.set("content-security-policy", policy === null
|
|
20
|
+
? "frame-ancestors 'none'"
|
|
21
|
+
: policy.replace(/frame-ancestors[^;]*/g, "frame-ancestors 'none'") +
|
|
22
|
+
(policy.includes("frame-ancestors") ? "" : "; frame-ancestors 'none'"));
|
|
17
23
|
}
|
|
18
24
|
return response;
|
|
19
25
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -24,6 +24,7 @@ export { AGENT_CONNECT_HANDLER, AGENT_CONNECT_PAGE } from "./agent.js";
|
|
|
24
24
|
export type { AgentIdentities, AgentIdentity, AgentPairing } from "./agent.js";
|
|
25
25
|
export type { AgentAuthorizationRequest, AgentAuthorizations, AgentScope } from "./oauth.js";
|
|
26
26
|
export type { ApiIssue } from "./errors.js";
|
|
27
|
+
export type { JobReadOptions, JobRun, JobRunOptions, JobsClient } from "./jobs.js";
|
|
27
28
|
export type { CurrentUser } from "./me.js";
|
|
28
29
|
export type { AuthRouteHandler, AuthRouteHandlers, SignOutOptions } from "./neon.js";
|
|
29
30
|
export type { ActionCall, ActionOptions, ActionResponse, Change, Edit, EditKind, EntityClient, FieldFilter, GeneratedActions, HistoryEntry, HistoryOptions, HistoryPage, JsonRecord, JsonValue, ManyFilter, ModelAggregateClient, OntologyClient, AggregateEntityClient, OntologyPage, OntologyQuery, OrderBy, RequestOptions, UntypedOntologyClient, ValidationIssue, Where, } from "./ontology.js";
|
package/dist/jobs.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/** A pipeline run, returned immediately by `run()` and refreshed with `getRun()`. */
|
|
2
|
+
export interface JobRun {
|
|
3
|
+
readonly id: string;
|
|
4
|
+
readonly deploymentId: string;
|
|
5
|
+
readonly jobName: string;
|
|
6
|
+
/** `queued`, `starting`, `running`, `succeeded`, `failed` or `cancelled`; future values are accepted. */
|
|
7
|
+
readonly status: string;
|
|
8
|
+
readonly trigger: string;
|
|
9
|
+
readonly partitionRange?: {
|
|
10
|
+
readonly startDate: string;
|
|
11
|
+
readonly endDate: string;
|
|
12
|
+
};
|
|
13
|
+
readonly failure?: {
|
|
14
|
+
readonly code: string;
|
|
15
|
+
readonly detail: string;
|
|
16
|
+
readonly retryable: boolean;
|
|
17
|
+
readonly reference: string;
|
|
18
|
+
};
|
|
19
|
+
readonly createdAt: string;
|
|
20
|
+
readonly updatedAt: string;
|
|
21
|
+
readonly startedAt?: string;
|
|
22
|
+
readonly completedAt?: string;
|
|
23
|
+
}
|
|
24
|
+
export interface JobRunOptions {
|
|
25
|
+
/** Reuse the same key when retrying one submission. Defaults to a new UUID for each call. */
|
|
26
|
+
readonly idempotencyKey?: string;
|
|
27
|
+
readonly partitionRange?: {
|
|
28
|
+
readonly startDate: string;
|
|
29
|
+
readonly endDate: string;
|
|
30
|
+
};
|
|
31
|
+
readonly signal?: AbortSignal;
|
|
32
|
+
}
|
|
33
|
+
export interface JobReadOptions {
|
|
34
|
+
readonly signal?: AbortSignal;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Server-only pipeline execution. The workspace credential can start and read only jobs deployed
|
|
38
|
+
* with `allow_app_runs=True`. Check the app user's permissions before calling on their behalf.
|
|
39
|
+
*/
|
|
40
|
+
export interface JobsClient {
|
|
41
|
+
/** Submit a run without waiting for completion. */
|
|
42
|
+
readonly run: (jobName: string, options?: JobRunOptions) => Promise<JobRun>;
|
|
43
|
+
readonly getRun: (runId: string, options?: JobReadOptions) => Promise<JobRun>;
|
|
44
|
+
readonly listRuns: (jobName: string, options?: JobReadOptions) => Promise<readonly JobRun[]>;
|
|
45
|
+
}
|
|
46
|
+
export declare function createJobsClient(dependencies: {
|
|
47
|
+
readonly workspace: string;
|
|
48
|
+
readonly fetch: (path: string, init: RequestInit) => Promise<Response>;
|
|
49
|
+
}): JobsClient;
|
package/dist/jobs.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { apiErrorFrom, readJson } from "./errors.js";
|
|
2
|
+
export function createJobsClient(dependencies) {
|
|
3
|
+
async function request(path, init) {
|
|
4
|
+
const response = await dependencies.fetch(path, init);
|
|
5
|
+
const payload = await readJson(response);
|
|
6
|
+
if (!response.ok) {
|
|
7
|
+
throw apiErrorFrom(response.status, payload, "SegmentStream rejected the job request.");
|
|
8
|
+
}
|
|
9
|
+
return payload;
|
|
10
|
+
}
|
|
11
|
+
return {
|
|
12
|
+
async run(jobName, options = {}) {
|
|
13
|
+
const key = options.idempotencyKey?.trim() ?? crypto.randomUUID();
|
|
14
|
+
if (!/^[A-Za-z0-9._:-]{8,128}$/.test(key)) {
|
|
15
|
+
throw new TypeError("idempotencyKey must be 8–128 letters, numbers, dots, underscores, colons or hyphens.");
|
|
16
|
+
}
|
|
17
|
+
const { run } = await request(`${dependencies.workspace}/jobs/${segment(jobName, "jobName")}/runs`, {
|
|
18
|
+
method: "POST",
|
|
19
|
+
headers: { "content-type": "application/json", "idempotency-key": key },
|
|
20
|
+
body: JSON.stringify(options.partitionRange ? { partitionRange: options.partitionRange } : {}),
|
|
21
|
+
signal: options.signal,
|
|
22
|
+
});
|
|
23
|
+
return run;
|
|
24
|
+
},
|
|
25
|
+
async getRun(runId, options = {}) {
|
|
26
|
+
const { run } = await request(`${dependencies.workspace}/runs/${segment(runId, "runId")}?includeTrigger=true`, { method: "GET", signal: options.signal });
|
|
27
|
+
return run;
|
|
28
|
+
},
|
|
29
|
+
async listRuns(jobName, options = {}) {
|
|
30
|
+
const { runs } = await request(`${dependencies.workspace}/jobs/${segment(jobName, "jobName")}/runs?includeTrigger=true`, { method: "GET", signal: options.signal });
|
|
31
|
+
return runs;
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function segment(value, name) {
|
|
36
|
+
if (!value || value === "." || value === "..") {
|
|
37
|
+
throw new TypeError(`${name} must be a non-empty identifier.`);
|
|
38
|
+
}
|
|
39
|
+
return encodeURIComponent(value);
|
|
40
|
+
}
|
package/dist/logo.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const SEGMENTSTREAM_LOGO_URI: string;
|
package/dist/logo.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Official asset: apps/website/public/segmentstream-logo-black.svg. Keep it in sync.
|
|
2
|
+
const SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="283 330 755 162" fill="none">
|
|
3
|
+
<!-- S icon (black) -->
|
|
4
|
+
<path fill="#18181b" d="M342.57,485.52l46.86-61.07c4.2-5.48,3.17-13.33-2.31-17.54l-1.08-0.83c-6.34-4.86-15.42-3.67-20.29,2.67l-29.25,38.12c-9.07,11.82-6.84,28.75,4.98,37.82L342.57,485.52z"/>
|
|
5
|
+
<path fill="#18181b" d="M393.15,410.11c-0.16-15.51-12.79-28.04-28.34-28.04H349.9c-4.94,0-8.95,4.01-8.95,8.95v19.39h42.52l0.01,0.01c0.09,0,0.19-0.01,0.28-0.01c4.37,0,7.9,3.54,7.9,7.9c0,0.9-0.15,1.77-0.43,2.58c0,0,1.25-3.04,1.63-5.72c0.24-1.69,0.29-3.97,0.3-4.77v0c0-0.03,0-0.06,0-0.09C393.15,410.18,393.15,410.11,393.15,410.11z"/>
|
|
6
|
+
<path fill="#18181b" d="M339.43,335.27l-46.86,61.07c-4.2,5.48-3.17,13.33,2.31,17.54l1.08,0.83c6.34,4.86,15.42,3.67,20.29-2.67l29.25-38.12c9.07-11.82,6.84-28.75-4.98-37.82L339.43,335.27z"/>
|
|
7
|
+
<path fill="#18181b" d="M288.85,410.68c0.16,15.51,12.79,28.04,28.34,28.04h14.91c4.94,0,8.95-4.01,8.95-8.95v-19.39h-42.52l-0.01-0.01c-0.09,0-0.19,0.01-0.28,0.01c-4.37,0-7.9-3.54-7.9-7.9c0-0.9,0.15-1.77,0.43-2.58c0,0-1.25,3.04-1.63,5.72c-0.24,1.69-0.29,3.97-0.3,4.77v0c0,0.03,0,0.06,0,0.09C288.85,410.6,288.85,410.68,288.85,410.68z"/>
|
|
8
|
+
<!-- Wordmark: segmentstream (black) -->
|
|
9
|
+
<g fill="#18181b">
|
|
10
|
+
<path d="M452.61,400.96c-1.03-1.65-2.99-3.72-6.2-3.72c-1.45,0-2.48,0.52-3.1,1.14c-0.52,0.52-0.93,1.34-0.93,2.27c0,1.14,0.52,1.86,1.45,2.48c0.62,0.41,1.24,0.62,3.41,1.76l5.16,2.58c2.27,1.14,4.44,2.27,6.09,4.03c2.38,2.38,3.2,5.06,3.2,8.06c0,3.92-1.45,7.33-3.82,9.91c-3.41,3.72-7.64,4.85-12.19,4.85c-2.79,0-6.3-0.41-9.4-2.79c-2.48-1.86-4.54-4.85-5.58-7.95l8.26-3.92c0.52,1.45,1.45,3.3,2.38,4.13c0.62,0.62,2.07,1.65,4.44,1.65c1.55,0,3.2-0.52,4.13-1.45c0.83-0.83,1.45-2.17,1.45-3.41c0-1.24-0.41-2.27-1.34-3.3c-1.24-1.24-2.68-1.96-4.34-2.68l-3.72-1.76c-1.96-0.93-4.34-2.07-5.99-3.72c-2.27-2.17-3.1-4.54-3.1-7.33c0-3.51,1.34-6.92,3.61-9.29c1.96-2.07,5.27-4.13,10.64-4.13c3.1,0,5.58,0.62,8.05,2.27c1.45,0.93,3.41,2.58,4.96,5.37L452.61,400.96z"/>
|
|
11
|
+
<path d="M509.71,423.06c-1.86,3.3-4.54,6.2-7.33,7.95c-3.51,2.27-8.16,3.3-12.81,3.3c-5.78,0-10.53-1.45-14.66-5.58c-4.13-4.13-6.4-10.12-6.4-16.63c0-6.71,2.37-13.22,6.92-17.86c3.61-3.61,8.36-5.89,14.56-5.89c6.92,0,11.36,2.99,13.94,5.68c5.58,5.78,6.09,13.84,6.09,18.38v1.24h-31.19c0.21,3.1,1.55,6.61,3.72,8.78c2.38,2.38,5.27,2.79,7.74,2.79c2.79,0,4.85-0.62,6.92-2.17c1.76-1.34,3.1-3.1,4.03-4.75L509.71,423.06z M499.7,405.4c-0.41-2.38-1.55-4.44-3.1-5.89c-1.34-1.24-3.51-2.48-6.82-2.48c-3.51,0-5.78,1.45-7.12,2.79c-1.45,1.34-2.58,3.41-3.1,5.58H499.7z"/>
|
|
12
|
+
<path d="M552.37,389.5h10.12v39.96c0,10.22-1.14,17.55-7.13,22.62c-2.79,2.27-7.33,4.65-14.46,4.65c-5.06,0-10.12-1.24-14.25-5.37c-3.2-3.2-5.68-8.26-6.4-13.53h9.81c0.41,2.38,1.65,5.16,3.2,6.81c1.24,1.34,3.72,3,7.85,3c4.23,0,6.71-1.65,7.95-2.89c3.3-3.2,3.3-7.75,3.3-10.95v-5.68c-4.65,5.89-10.33,6.2-13.22,6.2c-5.58,0-10.22-1.55-14.35-5.89c-3.92-4.13-6.51-10.02-6.51-17.25c0-7.95,3-13.42,6.4-16.83c4.34-4.44,9.5-5.99,14.77-5.99c4.34,0,9.19,1.45,12.91,5.89V389.5z M533.05,400.34c-2.58,2.38-4.44,6.4-4.44,11.05c0,4.34,1.65,8.57,4.34,10.95c1.86,1.65,4.65,2.89,7.75,2.89c3.2,0,5.99-0.93,8.47-3.51c2.99-3.2,3.82-6.92,3.82-10.22c0-4.85-1.55-8.47-4.13-10.95c-2.48-2.48-5.27-3.1-8.16-3.1C537.91,397.45,535.33,398.17,533.05,400.34z"/>
|
|
13
|
+
<path d="M575.29,389.5h10.12v4.03c3.41-4.13,7.43-5.16,10.74-5.16c4.54,0,8.78,2.17,11.26,6.4c3.61-5.16,9.09-6.4,12.91-6.4c5.27,0,9.91,2.48,12.39,6.82c0.83,1.45,2.27,4.65,2.27,10.95v26.85h-10.12v-23.96c0-4.85-0.52-6.82-0.93-7.75c-0.62-1.65-2.17-3.82-5.78-3.82c-2.48,0-4.65,1.34-5.99,3.2c-1.76,2.48-1.96,6.2-1.96,9.91v22.41h-10.12v-23.96c0-4.85-0.52-6.82-0.93-7.75c-0.62-1.65-2.17-3.82-5.78-3.82c-2.48,0-4.65,1.34-5.99,3.2c-1.76,2.48-1.96,6.2-1.96,9.91v22.41h-10.12V389.5z"/>
|
|
14
|
+
<path d="M686.72,423.06c-1.86,3.3-4.54,6.2-7.33,7.95c-3.51,2.27-8.16,3.3-12.8,3.3c-5.78,0-10.53-1.45-14.66-5.58c-4.13-4.13-6.4-10.12-6.4-16.63c0-6.71,2.38-13.22,6.92-17.86c3.61-3.61,8.36-5.89,14.56-5.89c6.92,0,11.36,2.99,13.94,5.68c5.58,5.78,6.09,13.84,6.09,18.38v1.24h-31.19c0.21,3.1,1.55,6.61,3.72,8.78c2.38,2.38,5.27,2.79,7.75,2.79c2.79,0,4.85-0.62,6.92-2.17c1.76-1.34,3.1-3.1,4.03-4.75L686.72,423.06z M676.7,405.4c-0.41-2.38-1.55-4.44-3.1-5.89c-1.34-1.24-3.51-2.48-6.82-2.48c-3.51,0-5.78,1.45-7.13,2.79c-1.45,1.34-2.58,3.41-3.1,5.58H676.7z"/>
|
|
15
|
+
<path d="M697.46,389.5h10.12v4.03c4.34-4.85,9.29-5.16,11.77-5.16c8.05,0,11.46,4.03,12.81,5.89c1.65,2.38,2.68,5.37,2.68,12.08v26.64h-10.12v-24.16c0-11.05-4.13-11.36-7.95-11.36c-4.54,0-9.19,0.62-9.19,14.66v20.86h-10.12V389.5z"/>
|
|
16
|
+
<path d="M758.08,398.79v34.18h-10.12v-34.18h-4.23v-9.29h4.23v-15.9h10.12v15.9h7.75v9.29H758.08z"/>
|
|
17
|
+
<path d="M791.64,400.96c-1.03-1.65-2.99-3.72-6.2-3.72c-1.45,0-2.48,0.52-3.1,1.14c-0.52,0.52-0.93,1.34-0.93,2.27c0,1.14,0.52,1.86,1.45,2.48c0.62,0.41,1.24,0.62,3.41,1.76l5.16,2.58c2.27,1.14,4.44,2.27,6.09,4.03c2.38,2.38,3.2,5.06,3.2,8.06c0,3.92-1.45,7.33-3.82,9.91c-3.41,3.72-7.64,4.85-12.19,4.85c-2.79,0-6.3-0.41-9.4-2.79c-2.48-1.86-4.54-4.85-5.58-7.95l8.26-3.92c0.52,1.45,1.45,3.3,2.38,4.13c0.62,0.62,2.07,1.65,4.44,1.65c1.55,0,3.2-0.52,4.13-1.45c0.83-0.83,1.45-2.17,1.45-3.41c0-1.24-0.41-2.27-1.34-3.3c-1.24-1.24-2.69-1.96-4.34-2.68l-3.72-1.76c-1.96-0.93-4.34-2.07-5.99-3.72c-2.27-2.17-3.1-4.54-3.1-7.33c0-3.51,1.34-6.92,3.61-9.29c1.96-2.07,5.27-4.13,10.64-4.13c3.1,0,5.58,0.62,8.05,2.27c1.45,0.93,3.41,2.58,4.96,5.37L791.64,400.96z"/>
|
|
18
|
+
<path d="M820.25,398.79v34.18h-10.12v-34.18h-4.23v-9.29h4.23v-15.9h10.12v15.9h7.75v9.29H820.25z"/>
|
|
19
|
+
<path d="M834.81,389.5h10.12v3.92c1.14-1.34,2.79-2.79,4.23-3.61c1.96-1.14,3.92-1.45,6.2-1.45c2.48,0,5.16,0.41,7.95,2.07l-4.13,9.19c-2.27-1.45-4.13-1.55-5.16-1.55c-2.17,0-4.34,0.31-6.3,2.38c-2.79,2.99-2.79,7.13-2.79,10.02v22.51h-10.12V389.5z"/>
|
|
20
|
+
<path d="M904.83,423.06c-1.86,3.3-4.54,6.2-7.33,7.95c-3.51,2.27-8.16,3.3-12.8,3.3c-5.78,0-10.53-1.45-14.66-5.58c-4.13-4.13-6.4-10.12-6.4-16.63c0-6.71,2.38-13.22,6.92-17.86c3.61-3.61,8.36-5.89,14.56-5.89c6.92,0,11.36,2.99,13.94,5.68c5.58,5.78,6.09,13.84,6.09,18.38v1.24h-31.19c0.21,3.1,1.55,6.61,3.72,8.78c2.38,2.38,5.27,2.79,7.75,2.79c2.79,0,4.85-0.62,6.92-2.17c1.76-1.34,3.1-3.1,4.03-4.75L904.83,423.06z M894.81,405.4c-0.41-2.38-1.55-4.44-3.1-5.89c-1.34-1.24-3.51-2.48-6.82-2.48c-3.51,0-5.78,1.45-7.13,2.79c-1.45,1.34-2.58,3.41-3.1,5.58H894.81z"/>
|
|
21
|
+
<path d="M946.38,389.5h10.12v43.48h-10.12v-4.54c-4.44,5.16-9.91,5.89-13.01,5.89c-13.43,0-21.07-11.15-21.07-23.13c0-14.15,9.71-22.82,21.17-22.82c3.2,0,8.78,0.83,12.91,6.2V389.5z M922.63,411.39c0,7.54,4.75,13.84,12.08,13.84c6.4,0,12.29-4.65,12.29-13.74c0-9.5-5.89-14.04-12.29-14.04C927.38,397.45,922.63,403.64,922.63,411.39z"/>
|
|
22
|
+
<path d="M969.31,389.5h10.12v4.03c3.41-4.13,7.43-5.16,10.74-5.16c4.54,0,8.78,2.17,11.26,6.4c3.62-5.16,9.09-6.4,12.91-6.4c5.27,0,9.91,2.48,12.39,6.82c0.83,1.45,2.27,4.65,2.27,10.95v26.85h-10.12v-23.96c0-4.85-0.52-6.82-0.93-7.75c-0.62-1.65-2.17-3.82-5.78-3.82c-2.48,0-4.65,1.34-5.99,3.2c-1.76,2.48-1.96,6.2-1.96,9.91v22.41H994.1v-23.96c0-4.85-0.52-6.82-0.93-7.75c-0.62-1.65-2.17-3.82-5.78-3.82c-2.48,0-4.65,1.34-5.99,3.2c-1.76,2.48-1.96,6.2-1.96,9.91v22.41h-10.12V389.5z"/>
|
|
23
|
+
</g>
|
|
24
|
+
</svg>`;
|
|
25
|
+
export const SEGMENTSTREAM_LOGO_URI = `data:image/svg+xml,${encodeURIComponent(SVG)}`;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Keep a copyable PKCE-bound code visible while trying the client's loopback callback. */
|
|
2
|
+
export declare function oauthCompletion(options: {
|
|
3
|
+
readonly redirect?: string;
|
|
4
|
+
readonly approved?: boolean;
|
|
5
|
+
}): Response;
|
|
6
|
+
export declare function loopbackRedirect(value: string): boolean;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { SEGMENTSTREAM_LOGO_URI } from "./logo.js";
|
|
3
|
+
function escape(value) {
|
|
4
|
+
return value.replace(/[&<>"']/g, (character) => ({
|
|
5
|
+
"&": "&",
|
|
6
|
+
"<": "<",
|
|
7
|
+
">": ">",
|
|
8
|
+
'"': """,
|
|
9
|
+
"'": "'",
|
|
10
|
+
})[character]);
|
|
11
|
+
}
|
|
12
|
+
/** Keep a copyable PKCE-bound code visible while trying the client's loopback callback. */
|
|
13
|
+
export function oauthCompletion(options) {
|
|
14
|
+
const nonce = randomBytes(18).toString("base64");
|
|
15
|
+
const redirect = options.redirect;
|
|
16
|
+
const code = redirect === undefined ? null : new URL(redirect).searchParams.get("code");
|
|
17
|
+
const title = options.approved === false ? "Request denied" : "Authorization complete";
|
|
18
|
+
const manual = code === null
|
|
19
|
+
? "<p>You can return to your terminal.</p>"
|
|
20
|
+
: `
|
|
21
|
+
<p>Return to your terminal. If it is still waiting, copy and paste this code:</p>
|
|
22
|
+
<input id="code" aria-label="Authorization code" readonly value="${escape(code)}">
|
|
23
|
+
<button id="copy" type="button">Copy code</button><p id="status" role="status"></p>
|
|
24
|
+
<p><a href="${escape(redirect)}">Continue to the client</a></p>`;
|
|
25
|
+
const script = code === null
|
|
26
|
+
? ""
|
|
27
|
+
: `
|
|
28
|
+
document.getElementById('copy').addEventListener('click', async () => {
|
|
29
|
+
const input = document.getElementById('code');
|
|
30
|
+
try { await navigator.clipboard.writeText(input.value);
|
|
31
|
+
document.getElementById('status').textContent = 'Copied';
|
|
32
|
+
} catch { input.select(); document.getElementById('status').textContent = 'Select and copy the code'; }
|
|
33
|
+
});
|
|
34
|
+
// A frame attempts loopback without losing the fallback page on connection failure.
|
|
35
|
+
const frame = document.createElement('iframe'); frame.hidden = true;
|
|
36
|
+
frame.src = ${JSON.stringify(redirect).replace(/</g, "\\u003c")}; document.body.appendChild(frame);`;
|
|
37
|
+
return new Response(`<!doctype html><html lang="en"><meta charset="utf-8">
|
|
38
|
+
<meta name="viewport" content="width=device-width, initial-scale=1"><title>${title}</title>
|
|
39
|
+
<style nonce="${nonce}">body{font:16px system-ui;background:#fafafa;color:#18181b;margin:10vh auto;
|
|
40
|
+
max-width:480px;padding:24px}input,button{font:inherit;padding:12px;margin:8px 0}input{width:95%}</style>
|
|
41
|
+
<img src="${escape(SEGMENTSTREAM_LOGO_URI)}" alt="SegmentStream" width="210" height="45">
|
|
42
|
+
<h1>${title}</h1>${manual}<script nonce="${nonce}">${script}</script></html>`, {
|
|
43
|
+
headers: {
|
|
44
|
+
"content-type": "text/html; charset=utf-8",
|
|
45
|
+
"cache-control": "no-store",
|
|
46
|
+
"referrer-policy": "no-referrer",
|
|
47
|
+
"x-frame-options": "DENY",
|
|
48
|
+
"content-security-policy": `default-src 'none'; img-src data:; style-src 'nonce-${nonce}'; ` +
|
|
49
|
+
`script-src 'nonce-${nonce}'; frame-ancestors 'none'; base-uri 'none'; form-action 'none'; ` +
|
|
50
|
+
`frame-src ${redirect === undefined ? "'none'" : new URL(redirect).origin}`,
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
export function loopbackRedirect(value) {
|
|
55
|
+
const url = new URL(value);
|
|
56
|
+
return url.protocol === "http:" && ["127.0.0.1", "localhost", "[::1]"].includes(url.hostname);
|
|
57
|
+
}
|
package/dist/oauth.d.ts
CHANGED
|
@@ -30,6 +30,7 @@ export interface AgentAuthorizations {
|
|
|
30
30
|
* It takes `request`, `decision` (`approve` or `deny`) and, to allow reading only, `scope`
|
|
31
31
|
* (`ontology:read`), and sends the browser back to the agent.
|
|
32
32
|
*/
|
|
33
|
+
readonly deviceRequest: (userCode: string) => Promise<AgentAuthorizationRequest | null>;
|
|
33
34
|
readonly consentHandler: () => {
|
|
34
35
|
readonly POST: (request: Request) => Promise<Response>;
|
|
35
36
|
};
|
package/dist/oauth.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { NextResponse } from "next/server";
|
|
2
2
|
import { isSameOrigin } from "./decision-pages.js";
|
|
3
3
|
import { apiErrorFrom, readJson } from "./errors.js";
|
|
4
|
+
import { loopbackRedirect, oauthCompletion } from "./oauth-completion.js";
|
|
4
5
|
import { CONSENT_PAGE } from "./pages.js";
|
|
5
6
|
export { CONSENT_PAGE };
|
|
6
7
|
function parseRequest(payload) {
|
|
@@ -55,6 +56,21 @@ export function createAgentAuthorizations(transport) {
|
|
|
55
56
|
}
|
|
56
57
|
return parseRequest(payload);
|
|
57
58
|
},
|
|
59
|
+
async deviceRequest(userCode) {
|
|
60
|
+
const response = await transport.fetch("/oauth/device/lookup", {
|
|
61
|
+
method: "POST",
|
|
62
|
+
headers: { "content-type": "application/json" },
|
|
63
|
+
body: JSON.stringify({ user_code: userCode }),
|
|
64
|
+
});
|
|
65
|
+
const payload = await readJson(response);
|
|
66
|
+
if (response.status === 404) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
if (!response.ok) {
|
|
70
|
+
throw apiErrorFrom(response.status, payload, "The verification attempt was refused.");
|
|
71
|
+
}
|
|
72
|
+
return parseRequest(payload);
|
|
73
|
+
},
|
|
58
74
|
consentHandler: () => ({
|
|
59
75
|
async POST(request) {
|
|
60
76
|
if (!isSameOrigin(request)) {
|
|
@@ -62,11 +78,21 @@ export function createAgentAuthorizations(transport) {
|
|
|
62
78
|
}
|
|
63
79
|
const form = await request.formData();
|
|
64
80
|
const id = String(form.get("request") ?? "");
|
|
81
|
+
if (form.get("decision") === "lookup") {
|
|
82
|
+
const url = new URL(CONSENT_PAGE, request.url);
|
|
83
|
+
url.searchParams.set("device", "1");
|
|
84
|
+
url.searchParams.set("user_code", String(form.get("user_code") ?? "").slice(0, 32));
|
|
85
|
+
return NextResponse.redirect(url, 303);
|
|
86
|
+
}
|
|
87
|
+
const device = id.startsWith("device:");
|
|
65
88
|
const decision = form.get("decision") === "approve" ? "approve" : "deny";
|
|
66
|
-
const response = await transport.fetch(requestPath(id, decision), {
|
|
89
|
+
const response = await transport.fetch(device ? `/oauth/device/${decision}` : requestPath(id, decision), {
|
|
67
90
|
method: "POST",
|
|
68
91
|
headers: { "content-type": "application/json", accept: "application/json" },
|
|
69
|
-
body: JSON.stringify(
|
|
92
|
+
body: JSON.stringify({
|
|
93
|
+
...(device ? { user_code: id.slice(7) } : {}),
|
|
94
|
+
...(form.get("scope") === "ontology:read" ? { scope: "ontology:read" } : {}),
|
|
95
|
+
}),
|
|
70
96
|
});
|
|
71
97
|
const payload = await readJson(response);
|
|
72
98
|
if (response.status === 404) {
|
|
@@ -75,10 +101,16 @@ export function createAgentAuthorizations(transport) {
|
|
|
75
101
|
if (!response.ok) {
|
|
76
102
|
throw apiErrorFrom(response.status, payload, "The ontology service refused the decision.");
|
|
77
103
|
}
|
|
104
|
+
if (device) {
|
|
105
|
+
return oauthCompletion({ approved: decision === "approve" });
|
|
106
|
+
}
|
|
78
107
|
const redirect = payload?.redirect;
|
|
79
108
|
if (typeof redirect !== "string" || !URL.canParse(redirect)) {
|
|
80
109
|
throw apiErrorFrom(502, payload, "The ontology service answered without a redirect.");
|
|
81
110
|
}
|
|
111
|
+
if (decision === "approve" && loopbackRedirect(redirect)) {
|
|
112
|
+
return oauthCompletion({ redirect });
|
|
113
|
+
}
|
|
82
114
|
// 303: the agent's redirect URI receives a GET, whatever method brought the browser here.
|
|
83
115
|
return NextResponse.redirect(redirect, 303);
|
|
84
116
|
},
|
package/dist/ui/index.d.ts
CHANGED
|
@@ -13,3 +13,4 @@ export { isAllowed } from "../permissions.js";
|
|
|
13
13
|
export type { CurrentUser } from "../me.js";
|
|
14
14
|
export type { AgentPairing } from "../agent.js";
|
|
15
15
|
export { SegmentStreamApiError, NOT_INVITED, SIGN_IN_LINK_INVALID } from "../errors.js";
|
|
16
|
+
export { SegmentStreamLogo } from "./segmentstream-logo.js";
|
package/dist/ui/index.js
CHANGED
|
@@ -9,3 +9,4 @@ export { SignIn } from "./sign-in.js";
|
|
|
9
9
|
export { SIGN_IN_ERROR_MESSAGES, signInErrorCode, signInErrorMessage, useSignIn, } from "./use-sign-in.js";
|
|
10
10
|
export { isAllowed } from "../permissions.js";
|
|
11
11
|
export { SegmentStreamApiError, NOT_INVITED, SIGN_IN_LINK_INVALID } from "../errors.js";
|
|
12
|
+
export { SegmentStreamLogo } from "./segmentstream-logo.js";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import Image from "next/image";
|
|
3
|
+
import { SEGMENTSTREAM_LOGO_URI } from "../logo.js";
|
|
4
|
+
/** The official SegmentStream wordmark, also used on segmentstream.com. */
|
|
5
|
+
export function SegmentStreamLogo() {
|
|
6
|
+
return (_jsx(Image, { unoptimized: true, src: SEGMENTSTREAM_LOGO_URI, alt: "SegmentStream", width: 210, height: 45 }));
|
|
7
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@segmentstream-cloud/app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "Sign-in, users, permissions, the ontology client and warehouse data access for SegmentStream workspace apps built with Next.js.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "SegmentStream",
|