Skip to content
1 change: 1 addition & 0 deletions src/auth/credential.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,7 @@ export class CertCredential implements Credential {
private createAuthJwt_(): string {
const claims = {
scope: [
'https://www.googleapis.com/auth/cloud-platform',
'https://www.googleapis.com/auth/firebase.database',
'https://www.googleapis.com/auth/firebase.messaging',
'https://www.googleapis.com/auth/identitytoolkit',
Expand Down
12 changes: 12 additions & 0 deletions src/firebase-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {Database} from '@firebase/database';
import {DatabaseService} from './database/database';
import {Firestore} from '@google-cloud/firestore';
import {FirestoreService} from './firestore/firestore';
import {InstanceId} from './instance-id/instance-id';

/**
* Type representing a callback which is called every time an app lifecycle event occurs.
Expand Down Expand Up @@ -333,6 +334,17 @@ export class FirebaseApp {
return service.client;
}

/**
* Returns the InstanceId service instance associated with this app.
*
* @return {InstanceId} The InstanceId service instance of this app.
*/
public instanceId(): InstanceId {
return this.ensureService_('iid', () => {
return new InstanceId(this);
});
}

/**
* Returns the name of the FirebaseApp instance.
*
Expand Down
13 changes: 13 additions & 0 deletions src/firebase-namespace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {Messaging} from './messaging/messaging';
import {Storage} from './storage/storage';
import {Database} from '@firebase/database';
import {Firestore} from '@google-cloud/firestore';
import {InstanceId} from './instance-id/instance-id';

const DEFAULT_APP_NAME = '[DEFAULT]';

Expand Down Expand Up @@ -338,6 +339,18 @@ export class FirebaseNamespace {
return Object.assign(fn, require('@google-cloud/firestore'));
}

/**
* Gets the `InstanceId` service namespace. The returned namespace can be used to get the
* `Instance` service for the default app or an explicitly specified app.
*/
get instanceId(): FirebaseServiceNamespace<InstanceId> {
const ns: FirebaseNamespace = this;
let fn: FirebaseServiceNamespace<InstanceId> = (app?: FirebaseApp) => {
return ns.ensureApp(app).instanceId();
};
return Object.assign(fn, {InstanceId});
}

/**
* Initializes the FirebaseApp instance.
*
Expand Down
10 changes: 10 additions & 0 deletions src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ declare namespace admin {
function messaging(app?: admin.app.App): admin.messaging.Messaging;
function storage(app?: admin.app.App): admin.storage.Storage;
function firestore(app?: admin.app.App): admin.firestore.Firestore;
function instanceId(app?: admin.app.App): admin.instanceId.InstanceId;
function initializeApp(options: admin.AppOptions, name?: string): admin.app.App;
}

Expand All @@ -69,6 +70,7 @@ declare namespace admin.app {
auth(): admin.auth.Auth;
database(url?: string): admin.database.Database;
firestore(): admin.firestore.Firestore;
instanceId(): admin.instanceId.InstanceId;
messaging(): admin.messaging.Messaging;
storage(): admin.storage.Storage;
delete(): Promise<void>;
Expand Down Expand Up @@ -415,6 +417,14 @@ declare namespace admin.firestore {
export import setLogFunction = _firestore.setLogFunction;
}

declare namespace admin.instanceId {
interface InstanceId {
app: admin.app.App;

deleteInstanceId(instanceId: string): Promise<void>;
}
}

declare module 'firebase-admin' {
}

Expand Down
120 changes: 120 additions & 0 deletions src/instance-id/instance-id-request.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/*!
* Copyright 2017 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import {FirebaseApp} from '../firebase-app';
import {FirebaseError, FirebaseInstanceIdError, InstanceIdClientErrorCode} from '../utils/error';
import {
HttpMethod, SignedApiRequestHandler, ApiSettings,
} from '../utils/api-request';

import * as validator from '../utils/validator';

/** Firebase IID backend host. */
const FIREBASE_IID_HOST = 'console.firebase.google.com';
/** Firebase IID backend port number. */
const FIREBASE_IID_PORT = 443;
/** Firebase IID backend path. */
const FIREBASE_IID_PATH = '/v1/';
/** Firebase IID request timeout duration in milliseconds. */
const FIREBASE_IID_TIMEOUT = 10000;

/** HTTP error codes raised by the backend server. */
const ERROR_CODES = {
400: 'Malformed instance ID argument.',
401: 'Request not authorized.',
403: 'Project does not match instance ID or the client does not have sufficient privileges.',
404: 'Failed to find the instance ID.',
409: 'Already deleted.',
429: 'Request throttled out by the backend server.',
500: 'Internal server error.',
503: 'Backend servers are over capacity. Try again later.',
};

/**
* Class that provides mechanism to send requests to the Firebase Instance ID backend endpoints.
*/
export class FirebaseInstanceIdRequestHandler {

private host: string = FIREBASE_IID_HOST;
private port: number = FIREBASE_IID_PORT;
private timeout: number = FIREBASE_IID_TIMEOUT;
private signedApiRequestHandler: SignedApiRequestHandler;
private path: string;

/**
* @param {FirebaseApp} app The app used to fetch access tokens to sign API requests.
* @param {string} projectId A Firebase project ID string.
*
* @constructor
*/
constructor(app: FirebaseApp, projectId: string) {
this.signedApiRequestHandler = new SignedApiRequestHandler(app);
this.path = FIREBASE_IID_PATH + `project/${projectId}/instanceId/`;
}

public deleteInstanceId(instanceId: string): Promise<Object> {
if (!validator.isNonEmptyString(instanceId)) {
return Promise.reject(new FirebaseInstanceIdError(
InstanceIdClientErrorCode.INVALID_INSTANCE_ID,
'Instance ID must be a non-empty string.'
));
}
return this.invokeRequestHandler(new ApiSettings(instanceId, 'DELETE'));
}

/**
* Invokes the request handler based on the API settings object passed.
*
* @param {ApiSettings} apiSettings The API endpoint settings to apply to request and response.
* @param {Object} requestData The request data.
* @return {Promise<Object>} A promise that resolves with the response.
*/
private invokeRequestHandler(apiSettings: ApiSettings): Promise<Object> {
let path: string = this.path + apiSettings.getEndpoint();
let httpMethod: HttpMethod = apiSettings.getHttpMethod();
return Promise.resolve()
.then(() => {
return this.signedApiRequestHandler.sendRequest(
this.host, this.port, path, httpMethod, undefined, undefined, this.timeout);
})
.then((response) => {
return response;
})
.catch((response) => {
let error;
if (typeof response === 'object' && 'error' in response) {
error = response.error;
} else {
error = response;
}

if (error instanceof FirebaseError) {
// In case of timeouts and other network errors, the API request handler returns a
// FirebaseError wrapped in the response. Simply throw it here.
throw error;
}

let template: string = ERROR_CODES[response.statusCode];
let message: string;
if (template) {
message = `Instance ID "${apiSettings.getEndpoint()}": ${template}`;
} else {
message = JSON.stringify(error);
}
throw new FirebaseInstanceIdError(InstanceIdClientErrorCode.API_ERROR, message);
});
}
}
96 changes: 96 additions & 0 deletions src/instance-id/instance-id.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*!
* Copyright 2017 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import {FirebaseApp} from '../firebase-app';
import {FirebaseInstanceIdError, InstanceIdClientErrorCode} from '../utils/error';
import {FirebaseServiceInterface, FirebaseServiceInternalsInterface} from '../firebase-service';
import {FirebaseInstanceIdRequestHandler} from './instance-id-request';

import * as utils from '../utils/index';
import * as validator from '../utils/validator';

/**
* Internals of an InstanceId service instance.
*/
class InstanceIdInternals implements FirebaseServiceInternalsInterface {
/**
* Deletes the service and its associated resources.
*
* @return {Promise<()>} An empty Promise that will be fulfilled when the service is deleted.
*/
public delete(): Promise<void> {
// There are no resources to clean up
return Promise.resolve(undefined);
}
}

export class InstanceId implements FirebaseServiceInterface {
public INTERNAL: InstanceIdInternals = new InstanceIdInternals();

private app_: FirebaseApp;
private requestHandler: FirebaseInstanceIdRequestHandler;

/**
* @param {Object} app The app for this InstanceId service.
* @constructor
*/
constructor(app: FirebaseApp) {
if (!validator.isNonNullObject(app) || !('options' in app)) {
throw new FirebaseInstanceIdError(
InstanceIdClientErrorCode.INVALID_ARGUMENT,
'First argument passed to admin.instanceId() must be a valid Firebase app instance.'
);
}

const projectId: string = utils.getProjectId(app);
if (!validator.isNonEmptyString(projectId)) {
// Assert for an explicit projct ID (either via AppOptions or the cert itself).
throw new FirebaseInstanceIdError(
InstanceIdClientErrorCode.INVALID_PROJECT_ID,
'Failed to determine project ID for InstanceId. Initialize the '
+ 'SDK with service account credentials or set project ID as an app option. '
+ 'Alternatively set the GCLOUD_PROJECT environment variable.',
);
}

this.app_ = app;
this.requestHandler = new FirebaseInstanceIdRequestHandler(app, projectId);
}

/**
* Deletes the specified instance ID from Firebase. This can be used to delete an instance ID
* and associated user data from a Firebase project, pursuant to the General Data Protection
* Regulation (GDPR).
*
* @param {string} instanceId The instance ID to be deleted
* @return {Promise<void>} A promise that resolves when the instance ID is successfully deleted.
*/
public deleteInstanceId(instanceId: string): Promise<void> {
return this.requestHandler.deleteInstanceId(instanceId)
.then((result) => {
// Return nothing on success
});
}

/**
* Returns the app associated with this InstanceId instance.
*
* @return {FirebaseApp} The app associated with this InstanceId instance.
*/
get app(): FirebaseApp {
return this.app_;
}
}
10 changes: 5 additions & 5 deletions src/utils/api-request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {OutgoingHttpHeaders} from 'http';
import https = require('https');

/** Http method type definition. */
export type HttpMethod = 'GET' | 'POST';
export type HttpMethod = 'GET' | 'POST' | 'DELETE';
/** API callback function type definition. */
export type ApiCallbackFunction = (data: Object) => void;

Expand Down Expand Up @@ -224,11 +224,11 @@ export class SignedApiRequestHandler extends HttpRequestHandler {
port: number,
path: string,
httpMethod: HttpMethod,
data: Object,
headers: Object,
timeout: number): Promise<Object> {
data?: Object,
headers?: Object,
timeout?: number): Promise<Object> {
return this.app_.INTERNAL.getToken().then((accessTokenObj) => {
let headersCopy: Object = deepCopy(headers);
let headersCopy: Object = (headers && deepCopy(headers)) || {};
let authorizationHeaderKey = 'Authorization';
headersCopy[authorizationHeaderKey] = 'Bearer ' + accessTokenObj.accessToken;
return super.sendRequest(host, port, path, httpMethod, data, headersCopy, timeout);
Expand Down
34 changes: 34 additions & 0 deletions src/utils/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,21 @@ export class FirebaseFirestoreError extends FirebaseError {
}
}

/**
* Firebase instance ID error code structure. This extends FirebaseError.
*
* @param {ErrorInfo} info The error code info.
* @param {string} [message] The error message. This will override the default
* message if provided.
* @constructor
*/
export class FirebaseInstanceIdError extends FirebaseError {
constructor(info: ErrorInfo, message?: string) {
// Override default message if custom message provided.
super({code: 'instance-id/' + info.code, message: message || info.message});
}
}


/**
* Firebase Messaging error code structure. This extends PrefixedFirebaseError.
Expand Down Expand Up @@ -472,6 +487,25 @@ export class MessagingClientErrorCode {
};
};

export class InstanceIdClientErrorCode {
public static INVALID_ARGUMENT = {
code: 'invalid-argument',
message: 'Invalid argument provided.',
};
public static INVALID_PROJECT_ID = {
code: 'invalid-project-id',
message: 'Invalid project ID provided.',
};
public static INVALID_INSTANCE_ID = {
code: 'invalid-instance-id',
message: 'Invalid instance ID provided.',
};
public static API_ERROR = {
code: 'api-error',
message: 'Instance ID API call failed.',
};
}

/** @const {ServerToClientCode} Auth server to client enum error codes. */
const AUTH_SERVER_TO_CLIENT_CODE: ServerToClientCode = {
// Claims payload is too large.
Expand Down
2 changes: 2 additions & 0 deletions test/integration/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ var utils = require('./utils');
var app = require('./app');
var auth = require('./auth');
var database = require('./database');
var instanceId = require('./instance-id');
var messaging = require('./messaging');
var storage = require('./storage');
var firestore = require('./firestore');
Expand Down Expand Up @@ -239,6 +240,7 @@ return promptForUpdateRules(flags['overwrite'])
.then(_.partial(app.test, utils))
.then(_.partial(auth.test, utils))
.then(_.partial(database.test, utils))
.then(_.partial(instanceId.test, utils))
.then(_.partial(messaging.test, utils))
.then(_.partial(storage.test, utils))
.then(_.partial(firestore.test, utils))
Expand Down
Loading